---
title: Waves
format:
html:
theme: flatly
grid:
body-width: 1000px # Domyślnie jest to ok. 800-900px
margin-width: 250px
toc: true
toc-depth: 3
highlight-style: tango
code-line-numbers: true
code-fold: true
code-summary: "Show the code"
code-tools: true
code-block-bg: "rgba(42, 174, 42, 0.02)"
code-block-border-left: "#2aae2a"
code-language-label: true
css: styles.css
math: mathjax
self-contained: true
other-links:
- text: Main page
href: https://dchorazkiewicz.github.io/Mathematics_Physics_Lectures
---
## Springs and Mechanical Waves
Mechanical waves arise as a result of the application of simple harmonic motion rules to interconnected systems. At their core, such waves can be visualized as particles connected by springs, where each particle's motion influences the next. This elegant interplay between force, displacement, and energy transfer forms the foundation of mechanical wave dynamics.
To illustrate this concept, imagine a chain of small masses connected by springs. When one mass is displaced, the motion propagates through the entire system, creating a wave-like pattern. You can explore this phenomenon in action with a simulation of ten masses linked by springs:
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mechanical Waves Simulation</title>
<style>
.mech-wave-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.app-header {
text-align: center;
margin-bottom: 20px;
}
.app-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.6rem;
}
.app-header p {
margin: 0;
font-size: 0.95rem;
color: #666;
}
.canvas-container {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
cursor: crosshair;
overflow: hidden;
touch-action: none; /* Prevent scrolling while drawing */
}
#mechCanvas {
display: block;
width: 100%;
height: 400px;
background: #ffffff;
}
.drawing-hint {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
background-color: #fff;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
display: flex;
justify-content: space-between;
}
.val-display {
font-family: monospace;
color: #007bff;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
.actions-row {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
padding-top: 10px;
border-top: 1px solid #eee;
margin-top: 5px;
}
.btn {
padding: 8px 16px;
border: 1px solid transparent;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary { background-color: #007bff; color: white; }
.btn-primary:hover { background-color: #0056b3; }
.btn-secondary { background-color: #6c757d; color: white; }
.btn-secondary:hover { background-color: #5a6268; }
.btn-outline { background-color: transparent; border-color: #ced4da; color: #495057; }
.btn-outline:hover { background-color: #e9ecef; border-color: #adb5bd; }
.btn-pause { background-color: #dc3545; color: white; }
.btn-pause:hover { background-color: #c82333; }
.mode-indicator {
text-align: center;
font-size: 0.85rem;
margin-top: 5px;
font-style: italic;
color: #666;
height: 1.2em;
}
</style>
</head>
<body>
<div class="mech-wave-app">
<div class="app-header">
<h2>Chain of Harmonic Oscillators</h2>
<p>Transition from discrete system to continuous wave</p>
</div>
<div class="canvas-container" id="canvasContainer">
<canvas id="mechCanvas"></canvas>
<div class="drawing-hint" id="drawingHint">Drawing Mode: Drag to shape the wave</div>
</div>
<div class="mode-indicator" id="modeInfo">Simulation active. Press "Pause" to draw your own shape.</div>
<div class="controls-grid">
<div class="control-group">
<div class="control-label">
<label>Number of Masses (N)</label>
<span id="val-n" class="val-display">50</span>
</div>
<input type="range" id="slider-n" min="10" max="200" step="1" value="50">
</div>
<div class="control-group">
<div class="control-label">
<label>Spring Stiffness (k)</label>
<span id="val-k" class="val-display">0.050</span>
</div>
<input type="range" id="slider-k" min="0.005" max="0.100" step="0.005" value="0.050">
</div>
<div class="control-group">
<div class="control-label">
<label>Damping</label>
<span id="val-damp" class="val-display">0.00005</span>
</div>
<!-- Scaled down by 100x -->
<input type="range" id="slider-damp" min="0.00000" max="0.00050" step="0.00001" value="0.00005">
</div>
<div class="actions-row">
<button id="btn-play" class="btn btn-pause">Pause</button>
<button id="btn-reset" class="btn btn-secondary">Reset (Flat)</button>
<div style="width: 10px;"></div> <!-- Spacer -->
<button class="btn btn-outline" onclick="setShape('gauss')">Gaussian Pulse</button>
<button class="btn btn-outline" onclick="setShape('sine')">Sine Pulse</button>
<button class="btn btn-outline" onclick="setShape('triangle')">Triangle Pulse</button>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('mechCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('canvasContainer');
const drawingHint = document.getElementById('drawingHint');
const modeInfo = document.getElementById('modeInfo');
// UI Refs
const sliderN = document.getElementById('slider-n');
const sliderK = document.getElementById('slider-k');
const sliderDamp = document.getElementById('slider-damp');
const valN = document.getElementById('val-n');
const valK = document.getElementById('val-k');
const valDamp = document.getElementById('val-damp');
const btnPlay = document.getElementById('btn-play');
const btnReset = document.getElementById('btn-reset');
// Physics State
let N = 50;
let k = 0.05; // Tension/Stiffness parameter (Scaled down)
let damping = 0.00005; // Damping scaled down 100x
let positions = []; // Current Y positions
let velocities = []; // Current Y velocities
let isRunning = true;
let isDrawing = false;
let animationId;
let width, height;
function initArray(size) {
N = size;
positions = new Float32Array(N);
velocities = new Float32Array(N);
// Initialize flat
for(let i=0; i<N; i++) {
positions[i] = 0;
velocities[i] = 0;
}
}
function resize() {
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
width = rect.width;
height = rect.height;
}
// Verlet integration / Semi-implicit Euler for coupled oscillators
// Eq: a[i] = k * (y[i-1] + y[i+1] - 2*y[i]) - damping * v[i]
function updatePhysics() {
if (!isRunning && !isDrawing) return;
if (isDrawing) return; // Do not update physics while drawing
// Multiple steps for stability and speed
const subSteps = 5;
for (let step = 0; step < subSteps; step++) {
// Calculate accelerations
const accelerations = new Float32Array(N);
for (let i = 1; i < N - 1; i++) {
const force = k * (positions[i-1] + positions[i+1] - 2*positions[i]);
const dampForce = -damping * velocities[i];
accelerations[i] = force + dampForce;
}
// Update positions and velocities
for (let i = 1; i < N - 1; i++) {
velocities[i] += accelerations[i];
positions[i] += velocities[i];
}
// Fixed ends
positions[0] = 0;
positions[N-1] = 0;
}
}
function draw() {
ctx.clearRect(0, 0, width, height);
const centerY = height / 2;
const spacing = width / (N - 1);
// Draw equilibrium line
ctx.beginPath();
ctx.moveTo(0, centerY);
ctx.lineTo(width, centerY);
ctx.strokeStyle = '#e9ecef';
ctx.lineWidth = 1;
ctx.stroke();
// Draw Springs (Connecting lines)
ctx.beginPath();
ctx.strokeStyle = '#007bff';
ctx.lineWidth = 2;
ctx.moveTo(0, centerY + positions[0]);
for (let i = 1; i < N; i++) {
const x = i * spacing;
const y = centerY - positions[i]; // Invert Y so positive is up
ctx.lineTo(x, y);
}
ctx.stroke();
// Draw Masses (Dots)
ctx.fillStyle = '#dc3545';
const radius = N > 100 ? 1.5 : (N > 50 ? 2.5 : 4);
for (let i = 0; i < N; i++) {
const x = i * spacing;
const y = centerY - positions[i];
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
// Draw "Fixed" mounts
ctx.fillStyle = '#333';
ctx.fillRect(0, centerY - 10, 4, 20);
ctx.fillRect(width - 4, centerY - 10, 4, 20);
}
function loop() {
updatePhysics();
draw();
animationId = requestAnimationFrame(loop);
}
// --- Interaction ---
window.setShape = function(type) {
// Stop, reset velocities, set positions
isRunning = false;
updatePlayBtn();
// Reset state
for(let i=0; i<N; i++) {
velocities[i] = 0;
positions[i] = 0;
}
const center = Math.floor(N / 2);
const amplitude = 100;
if (type === 'gauss') {
const sigma = N / 10;
for (let i = 1; i < N - 1; i++) {
positions[i] = amplitude * Math.exp(-Math.pow(i - center, 2) / (2 * sigma * sigma));
}
} else if (type === 'sine') {
for (let i = 1; i < N - 1; i++) {
// One full sine wave? Or half? Let's do a pulse
positions[i] = amplitude * Math.sin(Math.PI * i / (N-1));
}
} else if (type === 'triangle') {
const w = Math.floor(N / 4);
for (let i = 1; i < N - 1; i++) {
if (i > center - w && i < center + w) {
positions[i] = amplitude * (1 - Math.abs(i - center) / w);
}
}
}
draw();
modeInfo.textContent = "Shape set. Press Start to run.";
};
function handleDrawing(e) {
const rect = canvas.getBoundingClientRect();
// Handle touch or mouse
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
if (clientX === undefined) return;
const mouseX = clientX - rect.left;
const mouseY = clientY - rect.top;
// Map mouseX to index
const spacing = rect.width / (N - 1);
let idx = Math.round(mouseX / spacing);
// Clamp index
if (idx < 1) idx = 1;
if (idx >= N - 1) idx = N - 2; // Keep ends fixed
// Map mouseY to displacement
// Canvas Y is down, Physics Y is up. CenterY is 0 physics.
const centerY = rect.height / 2;
const displacement = -(mouseY - centerY); // Invert
// Set position
positions[idx] = displacement;
velocities[idx] = 0; // Stop it while drawing
// Smoothing neighbor (optional, makes drawing nicer)
if (idx > 1) positions[idx-1] = (positions[idx-1] + positions[idx]) / 2;
if (idx < N-2) positions[idx+1] = (positions[idx+1] + positions[idx]) / 2;
}
// Mouse Events
container.addEventListener('mousedown', (e) => {
if (isRunning) return; // Only draw when paused
isDrawing = true;
drawingHint.style.opacity = 1;
handleDrawing(e);
});
window.addEventListener('mousemove', (e) => {
if (isDrawing) handleDrawing(e);
});
window.addEventListener('mouseup', () => {
if (isDrawing) {
isDrawing = false;
drawingHint.style.opacity = 0;
}
});
// Touch Events
container.addEventListener('touchstart', (e) => {
if (isRunning) return;
isDrawing = true;
drawingHint.style.opacity = 1;
handleDrawing(e);
}, {passive: false});
window.addEventListener('touchmove', (e) => {
if (isDrawing) {
e.preventDefault(); // Prevent scroll
handleDrawing(e);
}
}, {passive: false});
window.addEventListener('touchend', () => {
isDrawing = false;
drawingHint.style.opacity = 0;
});
// Controls Logic
function updatePlayBtn() {
if (isRunning) {
btnPlay.textContent = "Pause";
btnPlay.className = "btn btn-pause";
modeInfo.textContent = "Simulation active...";
} else {
btnPlay.textContent = "Start";
btnPlay.className = "btn btn-primary";
modeInfo.textContent = "Paused. You can now draw on the graph with your mouse.";
}
}
btnPlay.addEventListener('click', () => {
isRunning = !isRunning;
updatePlayBtn();
});
btnReset.addEventListener('click', () => {
isRunning = false;
updatePlayBtn();
initArray(N);
draw();
modeInfo.textContent = "Reset. Draw a shape or choose a preset.";
});
sliderN.addEventListener('input', (e) => {
N = parseInt(e.target.value);
valN.textContent = N;
initArray(N);
isRunning = false;
updatePlayBtn();
draw();
});
sliderK.addEventListener('input', (e) => {
k = parseFloat(e.target.value);
valK.textContent = k.toFixed(3);
});
sliderDamp.addEventListener('input', (e) => {
damping = parseFloat(e.target.value);
valDamp.textContent = damping.toFixed(5);
});
// Start
resize();
initArray(50);
window.addEventListener('resize', resize);
loop();
})();
</script>
</body>
</html>
```
## Plane Harmonic Wave
A plane harmonic wave is a fundamental example of wave motion, described by a sinusoidal function such as:
$$
\psi(x, t) = A \sin(kx - \omega t + \phi),
$$
where:
- $A$ is the amplitude,
- $k$ is the wave number,
- $\omega$ is the angular frequency,
- $\phi$ is the phase constant,
- $x$ and $t$ represent position and time, respectively.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters for the wave
A = 1.0 # Amplitude
lambda_wave = 4.0 # Wavelength
k = 2 * np.pi / lambda_wave # Wave number
phi = 0 # Phase constant
x = np.linspace(0, 8, 1000) # Range for x
# Wave function
psi = A * np.sin(k * x - phi)
# Plot the wave function
plt.figure(figsize=(12, 6))
plt.plot(x, psi, label="$\\psi(x) = A \\sin(kx - \\phi)$", color="blue")
# Mark the amplitude
plt.plot([lambda_wave / 4, lambda_wave / 4], [0, A], color="green", linewidth=2)
plt.text(lambda_wave / 4 + 0.2, A / 2, r'Amplitude $A$', fontsize=12, ha='left', color="green")
# Mark the segment from 1 to 5 with vertical lines at the ends
x_start = 1
x_end = 5
y_label = A + 0.3 # Position above the wave to mark the segment
plt.plot([x_start, x_end], [y_label, y_label], color="red", linewidth=2) # Horizontal line
plt.plot([x_start, x_start], [y_label - 0.05, y_label + 0.05], color="red", linewidth=2) # Vertical line at start
plt.plot([x_end, x_end], [y_label - 0.05, y_label + 0.05], color="red", linewidth=2) # Vertical line at end
plt.text((x_start + x_end) / 2, y_label - 0.1, "$\\lambda$", fontsize=12, ha='center', color="red") # Label above the segment
# Add auxiliary lines
plt.axhline(y=0, color='gray', linestyle='--', linewidth=0.7, alpha=0.5) # Horizontal axis
plt.axhline(y=A, color='gray', linestyle='--', linewidth=0.7) # Amplitude line
plt.axhline(y=-A, color='gray', linestyle='--', linewidth=0.7) # Negative amplitude line
# Configure axes
plt.xlabel("Position $x$", fontsize=14)
plt.ylabel("Wave function $\\psi(x)$", fontsize=14)
# Add grid
plt.grid(True, linestyle="--", alpha=0.7)
# Display the plot
plt.show()
```
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plane Wave Simulation</title>
<style>
/* Reset and main container styles - isolated */
.wave-sim-wrapper {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.wave-sim-wrapper * {
box-sizing: border-box;
}
/* Canvas styling */
.wave-canvas-container {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 20px;
position: relative;
/* Container for canvas ensuring proper aspect ratio/sizing logic if needed */
}
#waveCanvas {
display: block;
width: 100%;
height: 350px; /* Increased height for clearer axis labels */
cursor: crosshair;
}
/* Control panel */
.wave-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 600px) {
.wave-controls {
grid-template-columns: 1fr;
}
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
font-size: 0.9em;
font-weight: 600;
color: #495057;
display: flex;
justify-content: space-between;
}
.control-group span.value-display {
font-weight: normal;
color: #007bff;
font-family: monospace;
}
/* Slider Styles */
.wave-sim-wrapper input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
.wave-sim-wrapper input[type=range]:focus { outline: none; }
.wave-sim-wrapper input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px; width: 16px; border-radius: 50%;
background: #007bff; cursor: pointer; margin-top: -6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.wave-sim-wrapper input[type=range]::-webkit-slider-runnable-track {
width: 100%; height: 4px; cursor: pointer;
background: #dee2e6; border-radius: 2px;
}
.wave-sim-wrapper input[type=range]::-moz-range-thumb {
height: 16px; width: 16px; border: none; border-radius: 50%;
background: #007bff; cursor: pointer;
}
.wave-sim-wrapper input[type=range]::-moz-range-track {
width: 100%; height: 4px; cursor: pointer;
background: #dee2e6; border-radius: 2px;
}
/* Buttons */
.button-container {
grid-column: 1 / -1;
display: flex;
justify-content: center;
margin-top: 10px;
}
.wave-btn {
background-color: #28a745;
color: white;
border: none;
padding: 10px 24px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
font-weight: 600;
}
.wave-btn:hover { background-color: #218838; }
.wave-btn.paused { background-color: #dc3545; }
.wave-btn.paused:hover { background-color: #c82333; }
.equation-display {
grid-column: 1 / -1;
text-align: center;
font-family: 'Times New Roman', serif;
font-style: italic;
font-size: 1.2em;
margin-bottom: 10px;
color: #333;
}
.legend {
grid-column: 1 / -1;
text-align: center;
font-size: 0.85em;
color: #666;
margin-top: -10px;
}
</style>
</head>
<body>
<div class="wave-sim-wrapper">
<div class="equation-display">
y(x, t) = A sin(kx - ωt)
</div>
<div class="wave-canvas-container">
<canvas id="waveCanvas"></canvas>
</div>
<div class="wave-controls">
<div class="control-group">
<label for="amplitude">Amplitude (A) [m]: <span id="val-amp" class="value-display">1.0</span></label>
<input type="range" id="amplitude" min="0" max="2.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label for="wavelength">Wavelength (λ) [m]: <span id="val-lambda" class="value-display">4.0</span></label>
<input type="range" id="wavelength" min="1.0" max="8.0" step="0.1" value="4.0">
</div>
<div class="control-group">
<label for="omega">Angular Frequency (ω) [rad/s]: <span id="val-omega" class="value-display">2.0</span></label>
<input type="range" id="omega" min="0" max="10" step="0.1" value="2.0">
</div>
<div class="button-container">
<button id="toggleBtn" class="wave-btn">Pause</button>
</div>
<div class="legend">
Press <strong>Pause</strong> to see measurements for Amplitude and Wavelength.
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('waveCanvas');
const ctx = canvas.getContext('2d');
// UI Elements
const ampInput = document.getElementById('amplitude');
const lambdaInput = document.getElementById('wavelength');
const omegaInput = document.getElementById('omega');
const toggleBtn = document.getElementById('toggleBtn');
const valAmp = document.getElementById('val-amp');
const valLambda = document.getElementById('val-lambda');
const valOmega = document.getElementById('val-omega');
// State variables
let width, height;
let animationId;
let isRunning = true;
let time = 0;
let lastTime = performance.now();
// Simulation parameters
let params = {
A: parseFloat(ampInput.value),
lambda: parseFloat(lambdaInput.value),
omega: parseFloat(omegaInput.value)
};
// Configuration
const PADDING_LEFT = 50; // Space for Y-axis labels
const PADDING_BOTTOM = 40; // Space for X-axis labels
const PIXELS_PER_METER = 80; // Scale X
const PIXELS_PER_AMP_UNIT = 60; // Scale Y
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Logical dimensions
width = canvas.width / dpr;
height = canvas.height / dpr;
ctx.scale(dpr, dpr);
// Clear logical dimensions for internal logic
// We use `width` and `height` in draw function which refer to CSS pixels now
}
function drawGrid(xAxisY) {
ctx.strokeStyle = '#e9ecef';
ctx.lineWidth = 1;
ctx.fillStyle = '#6c757d';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
// X Axis Grid & Labels
const startX = PADDING_LEFT;
const endX = width;
const yZero = xAxisY;
// Physical length visible
const visibleMeters = (width - PADDING_LEFT) / PIXELS_PER_METER;
for (let m = 0; m <= visibleMeters; m += 1) {
const px = startX + m * PIXELS_PER_METER;
// Grid line
ctx.beginPath();
ctx.moveTo(px, 0);
ctx.lineTo(px, height - PADDING_BOTTOM);
ctx.stroke();
// Label
ctx.fillText(m.toString(), px, height - PADDING_BOTTOM + 5);
}
// Axis label
ctx.fillText("Position x (m)", width / 2 + PADDING_LEFT/2, height - 15);
// Y Axis Grid & Labels
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
const maxAmpVisible = (yZero) / PIXELS_PER_AMP_UNIT;
// Steps for Y axis (e.g., 0.5, 1.0, 1.5...)
for (let a = -Math.floor(maxAmpVisible); a <= Math.floor(maxAmpVisible); a += 0.5) {
if (a === 0) continue; // Skip 0, handled by main axis
const py = yZero - (a * PIXELS_PER_AMP_UNIT);
if (py > 0 && py < height - PADDING_BOTTOM) {
// Grid line
ctx.beginPath();
ctx.moveTo(PADDING_LEFT, py);
ctx.lineTo(width, py);
ctx.stroke();
// Label
ctx.fillText(a.toFixed(1), PADDING_LEFT - 10, py);
}
}
// Axis label logic
ctx.save();
ctx.translate(15, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText("Displacement y (m)", 0, 0);
ctx.restore();
}
function drawArrow(x1, y1, x2, y2, color = 'red', label = null) {
const headlen = 8; // length of head in pixels
const angle = Math.atan2(y2 - y1, x2 - x1);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// Arrow head 1
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - headlen * Math.cos(angle - Math.PI / 6), y2 - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(x2 - headlen * Math.cos(angle + Math.PI / 6), y2 - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
// Arrow head 2 (for double sided, if needed logic, but here manual)
}
function drawDoubleArrow(x1, y1, x2, y2, color, label) {
drawArrow(x1, y1, x2, y2, color);
drawArrow(x2, y2, x1, y1, color); // Reverse for other head
if (label) {
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
ctx.fillStyle = color;
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(label, midX, midY - 5);
}
}
function drawMeasurements(xAxisY, k) {
// Find peaks
// Peak condition: k*x - omega*t = PI/2 + 2*PI*n
// x = (PI/2 + omega*t + 2*PI*n) / k
const phase = params.omega * time;
const firstN = Math.floor((-phase - Math.PI/2) / (2*Math.PI));
// We search for n starting a bit before 0 to cover all visible area
let peaks = [];
// Scan for peaks within visible range
// Physical width
const maxPhysX = (width - PADDING_LEFT) / PIXELS_PER_METER;
for (let n = firstN; ; n++) {
const x_phys = (Math.PI/2 + phase + 2*Math.PI*n) / k;
if (x_phys > maxPhysX + params.lambda) break; // Optimization stop
if (x_phys >= 0 && x_phys <= maxPhysX) {
peaks.push(x_phys);
}
}
// Draw Amplitude (on the first visible peak)
if (peaks.length > 0) {
const pX = peaks[0];
const pxCanvas = PADDING_LEFT + pX * PIXELS_PER_METER;
const topY = xAxisY - (params.A * PIXELS_PER_AMP_UNIT);
// Vertical line for Amplitude
ctx.beginPath();
ctx.moveTo(pxCanvas, xAxisY);
ctx.lineTo(pxCanvas, topY);
ctx.strokeStyle = '#28a745';
ctx.lineWidth = 2;
ctx.stroke();
// Arrow head at top
ctx.beginPath();
ctx.moveTo(pxCanvas, topY);
ctx.lineTo(pxCanvas - 5, topY + 8);
ctx.lineTo(pxCanvas + 5, topY + 8);
ctx.closePath();
ctx.fillStyle = '#28a745';
ctx.fill();
// Label
ctx.fillStyle = '#28a745';
ctx.textAlign = 'left';
ctx.font = 'bold 13px sans-serif';
ctx.fillText(`A = ${params.A.toFixed(1)}m`, pxCanvas + 8, xAxisY - (params.A * PIXELS_PER_AMP_UNIT)/2);
}
// Draw Wavelength (between two peaks)
if (peaks.length >= 2) {
const p1 = peaks[0];
const p2 = peaks[1];
const x1 = PADDING_LEFT + p1 * PIXELS_PER_METER;
const x2 = PADDING_LEFT + p2 * PIXELS_PER_METER;
const yLevel = xAxisY - (params.A * PIXELS_PER_AMP_UNIT) - 20; // Above the wave
// Draw lines up
ctx.strokeStyle = 'rgba(255,0,0,0.3)';
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(x1, xAxisY - (params.A * PIXELS_PER_AMP_UNIT));
ctx.lineTo(x1, yLevel);
ctx.moveTo(x2, xAxisY - (params.A * PIXELS_PER_AMP_UNIT));
ctx.lineTo(x2, yLevel);
ctx.stroke();
ctx.setLineDash([]);
// Draw double arrow
drawDoubleArrow(x1, yLevel, x2, yLevel, '#dc3545', `λ = ${params.lambda.toFixed(1)}m`);
}
}
function draw() {
// Clear
ctx.clearRect(0, 0, width, height);
const xAxisY = (height - PADDING_BOTTOM) / 2;
// Draw Grid & Axes
drawGrid(xAxisY);
// Main Axis Line
ctx.beginPath();
ctx.moveTo(PADDING_LEFT, xAxisY);
ctx.lineTo(width, xAxisY);
ctx.strokeStyle = '#333';
ctx.lineWidth = 1.5;
ctx.stroke();
// Wave Drawing
ctx.beginPath();
ctx.strokeStyle = '#007bff';
ctx.lineWidth = 3;
const k = (2 * Math.PI) / params.lambda;
// Start drawing from x=0 (physically) which is PADDING_LEFT pixels
const startPx = PADDING_LEFT;
ctx.beginPath();
for (let px = startPx; px < width; px++) {
// Physical x relative to start of axis
const x_phys = (px - startPx) / PIXELS_PER_METER;
// Wave equation
const y_phys = params.A * Math.sin(k * x_phys - params.omega * time);
// Canvas Y
const py = xAxisY - (y_phys * PIXELS_PER_AMP_UNIT);
if (px === startPx) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
// Particle Visualization (at fixed physical position x = 2.0m)
const particleX_phys = 2.0;
if ((particleX_phys * PIXELS_PER_METER + PADDING_LEFT) < width) {
const particleY_phys = params.A * Math.sin(k * particleX_phys - params.omega * time);
const pPx = PADDING_LEFT + particleX_phys * PIXELS_PER_METER;
const pPy = xAxisY - (particleY_phys * PIXELS_PER_AMP_UNIT);
ctx.beginPath();
ctx.arc(pPx, pPy, 6, 0, Math.PI * 2);
ctx.fillStyle = '#dc3545';
ctx.fill();
// Helper line for particle
ctx.beginPath();
ctx.moveTo(pPx, xAxisY);
ctx.lineTo(pPx, pPy);
ctx.strokeStyle = 'rgba(220, 53, 69, 0.4)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw measurements if paused
if (!isRunning) {
drawMeasurements(xAxisY, k);
}
}
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
if (isRunning) {
time += dt;
}
draw();
animationId = requestAnimationFrame(animate);
}
// Listeners
window.addEventListener('resize', () => { resize(); draw(); });
ampInput.addEventListener('input', (e) => {
params.A = parseFloat(e.target.value);
valAmp.textContent = params.A.toFixed(1);
if (!isRunning) draw();
});
lambdaInput.addEventListener('input', (e) => {
params.lambda = parseFloat(e.target.value);
valLambda.textContent = params.lambda.toFixed(1);
if (!isRunning) draw();
});
omegaInput.addEventListener('input', (e) => {
params.omega = parseFloat(e.target.value);
valOmega.textContent = params.omega.toFixed(1);
if (!isRunning) draw();
});
toggleBtn.addEventListener('click', () => {
isRunning = !isRunning;
if (isRunning) {
toggleBtn.textContent = "Pause";
toggleBtn.classList.remove('paused');
lastTime = performance.now();
} else {
toggleBtn.textContent = "Resume";
toggleBtn.classList.add('paused');
draw(); // Force redraw to show arrows immediately
}
});
resize();
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
```
### Constructing the Wave Equation
The temporal and spatial derivatives of the wave function $\psi(x, t)$ are key to understanding its behavior. The first and second derivatives with respect to time $t$ are:
$$
\frac{\partial \psi}{\partial t} = -A \omega \cos(kx - \omega t + \phi),
$$
$$
\frac{\partial^2 \psi}{\partial t^2} = -A \omega^2 \sin(kx - \omega t + \phi).
$$
Similarly, the first and second derivatives with respect to position $x$ are:
$$
\frac{\partial \psi}{\partial x} = A k \cos(kx - \omega t + \phi),
$$
$$
\frac{\partial^2 \psi}{\partial x^2} = -A k^2 \sin(kx - \omega t + \phi).
$$
By combining these derivatives, we can see how the wave function satisfies the general wave equation. Substituting $\frac{\partial^2 \psi}{\partial x^2}$ and $\frac{\partial^2 \psi}{\partial t^2}$ into:
$$
\frac{\partial^2 \psi}{\partial x^2} = \frac{1}{v^2} \frac{\partial^2 \psi}{\partial t^2},
$$
and using the relations $v = \frac{\omega}{k}$, it becomes clear that the sinusoidal wave satisfies this fundamental equation, connecting temporal and spatial changes in the wave function.
### General Implications
What we have derived here is not just a coincidence but is, in fact, the result of more general considerations. The equation we obtained, often called the wave equation, emerges from the fundamental principles governing wave phenomena. It describes the behavior of a wide range of wave-like systems, including sound waves, water waves, and even electromagnetic waves in certain contexts. As such, it is recognized as one of the cornerstone equations in the study of physical systems exhibiting wave motion.
Let us begin! First, we will derive the difference equations that allow for the numerical solution of the wave equation. Here is the plan:
### Difference Equation for the Wave Equation
Recall the wave equation in the form:
$$
\frac{\partial^2 \psi}{\partial x^2} = \frac{1}{v^2} \frac{\partial^2 \psi}{\partial t^2}.
$$
#### Discretization of Space and Time
Assume that:
- We divide the space $x$ into $N$ equal intervals of length $\Delta x$.
- We divide the time $t$ into steps of length $\Delta t$.
The function $\psi(x, t)$ is replaced by a discrete grid $\psi_i^n$, where $i$ denotes the spatial index and $n$ denotes the time index:
- $i = 0, 1, 2, \dots, N$,
- $n = 0, 1, 2, \dots$.
#### Finite Difference Approximations
1. **Second spatial derivatives**:
Approximate $\frac{\partial^2 \psi}{\partial x^2}$ using central differences:
$$
\frac{\partial^2 \psi}{\partial x^2} \approx \frac{\psi_{i+1}^n - 2\psi_i^n + \psi_{i-1}^n}{\Delta x^2}.
$$
2. **Second temporal derivatives**:
Approximate $\frac{\partial^2 \psi}{\partial t^2}$ using central differences:
$$
\frac{\partial^2 \psi}{\partial t^2} \approx \frac{\psi_i^{n+1} - 2\psi_i^n + \psi_i^{n-1}}{\Delta t^2}.
$$
#### Difference Equation
Substituting these into the wave equation, we obtain:
$$
\frac{\psi_{i+1}^n - 2\psi_i^n + \psi_{i-1}^n}{\Delta x^2} = \frac{1}{v^2} \frac{\psi_i^{n+1} - 2\psi_i^n + \psi_i^{n-1}}{\Delta t^2}.
$$
Simplifying, we solve for $\psi_i^{n+1}$:
$$
\psi_i^{n+1} = 2\psi_i^n - \psi_i^{n-1} + c^2 \left( \psi_{i+1}^n - 2\psi_i^n + \psi_{i-1}^n \right),
$$
where $c = \frac{v \Delta t}{\Delta x}$ is the Courant number, which must satisfy the stability condition $c \leq 1$.
---
### Numerical Implementation
1. **Initial state**: A string fixed at both ends ($\psi_0^n = \psi_N^n = 0$) with an initial triangular displacement.
2. **Time iteration**: Compute the state $\psi_i^{n+1}$ based on the states $\psi_i^n$ and $\psi_i^{n-1}$.
3. **Visualization**: Display several snapshots of the wave at different time intervals.
Next, I will prepare Python code to implement this algorithm.
The following code implements an explicit solution of the wave equation for a string fixed at both ends with an initial triangular displacement. The visualization shows the wave's propagation at various time intervals.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
L = 1.0 # Length of the string
T = 1.0 # Total simulation time
c = 1.0 # Wave speed
nx = 100 # Number of spatial points
nt = 500 # Number of time steps
dx = L / (nx - 1) # Spatial step
dt = T / nt # Time step
r = c * dt / dx # Courant number
# Initialize arrays
u = np.zeros((nt, nx)) # Solution at successive time steps
x = np.linspace(0, L, nx) # x-coordinates
# Initial conditions: e.g., a sinusoidal-shaped initial displacement
u[0, :] = np.exp(-100 * (x - 0.5)**2)
u[1, :] = u[0, :]
# Boundary conditions: u(0, t) = u(L, t) = 0 (string fixed at ends)
u[:, 0] = 0
u[:, -1] = 0
# Time iteration
for n in range(1, nt - 1):
for i in range(1, nx - 1):
u[n + 1, i] = (2 * (1 - r**2) * u[n, i] -
u[n - 1, i] +
r**2 * (u[n, i + 1] + u[n, i - 1]))
# Select time snapshots for visualization
time_snapshots = [0, 10, 50, 100, 200, 300]
time_labels = [f'Time = {n*dt:.2f} s' for n in time_snapshots]
# Plot snapshots at selected time intervals
plt.figure(figsize=(10, 6))
for idx, n in enumerate(time_snapshots):
plt.plot(x, u[n, :], label=time_labels[idx])
plt.xlabel('Position on the string')
plt.ylabel('Amplitude')
plt.title('Wave propagation on a string at selected time intervals')
plt.legend()
plt.grid(True)
plt.show()
```
same we can save in gif file

```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>1D Wave Equation Solver</title>
<style>
/* Scoped styles for the widget */
.wave-solver-wrapper {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.wave-solver-wrapper * {
box-sizing: border-box;
}
.solver-header {
text-align: center;
margin-bottom: 20px;
}
.solver-header h2 {
margin: 0 0 10px 0;
color: #2c3e50;
font-size: 1.5rem;
}
.solver-header p {
margin: 0;
color: #666;
font-size: 0.95rem;
}
/* Canvas Area */
.solver-canvas-container {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 20px;
position: relative;
box-shadow: inset 0 0 10px rgba(0,0,0,0.02);
}
#solverCanvas {
display: block;
width: 100%;
height: 350px;
}
/* Controls */
.solver-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
align-items: end;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
font-size: 0.9em;
font-weight: 600;
color: #495057;
}
.wave-select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
background-color: white;
cursor: pointer;
width: 100%;
}
/* Toggle Switch */
.toggle-container {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #007bff;
}
input:checked + .slider:before {
transform: translateX(26px);
}
/* Buttons */
.btn-group {
display: flex;
gap: 10px;
}
.solver-btn {
padding: 10px 20px;
font-size: 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
flex: 1;
text-align: center;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover { background-color: #0056b3; }
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover { background-color: #bd2130; }
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover { background-color: #5a6268; }
.info-panel {
margin-top: 15px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
font-size: 0.9em;
color: #495057;
text-align: center;
font-family: monospace;
display: flex;
justify-content: space-around;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.solver-controls {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="wave-solver-wrapper">
<div class="solver-header">
<h2>Wave Equation Evolution</h2>
<p>Fixed ends: u(0,t) = u(L,t) = 0. Initial velocity: 0.</p>
</div>
<div class="solver-canvas-container">
<canvas id="solverCanvas"></canvas>
</div>
<div class="solver-controls">
<div class="control-group">
<label for="shapeSelect">Initial Pulse Shape:</label>
<select id="shapeSelect" class="wave-select">
<option value="triangle">Triangular (Pluck)</option>
<option value="gaussian">Gaussian Pulse</option>
<option value="sine">Sinusoidal (Mode 1)</option>
<option value="sine2">Sinusoidal (Mode 2)</option>
<option value="square">Square Pulse</option>
<option value="noise">Random Noise</option>
</select>
</div>
<div class="control-group">
<label for="speedRange">Simulation Speed:</label>
<input type="range" id="speedRange" min="1" max="5" value="1" step="1">
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox" id="showComponents">
<span class="slider"></span>
</label>
<span style="font-size: 0.9em; font-weight: 600; color: #495057;">Show Components</span>
</div>
</div>
<div class="btn-group">
<button id="startStopBtn" class="solver-btn btn-primary">Start</button>
<button id="resetBtn" class="solver-btn btn-secondary">Reset</button>
</div>
</div>
<div class="info-panel">
<span id="simInfo">t = 0.00</span>
<div id="legendContainer" style="display: none; gap: 15px;">
<span class="legend-item"><span class="legend-color" style="background: #28a745;"></span> Right Wave</span>
<span class="legend-item"><span class="legend-color" style="background: #fd7e14;"></span> Left Wave</span>
</div>
</div>
</div>
<script>
(function() {
// Configuration
const N = 400; // Number of spatial points
const C_VAL = 1.0; // Courant number (c * dt / dx). Set to 1.0 for exact 1D propagation
// State arrays (Current, Previous, Next)
let u = new Float64Array(N);
let u_prev = new Float64Array(N);
let u_next = new Float64Array(N);
let u_initial = new Float64Array(N); // Store initial state for decomposition
let isRunning = false;
let animationId;
let timeStepCount = 0;
let speedMultiplier = 1;
let showComponents = false;
// UI Elements
const canvas = document.getElementById('solverCanvas');
const ctx = canvas.getContext('2d');
const startStopBtn = document.getElementById('startStopBtn');
const resetBtn = document.getElementById('resetBtn');
const shapeSelect = document.getElementById('shapeSelect');
const speedRange = document.getElementById('speedRange');
const componentsToggle = document.getElementById('showComponents');
const simInfo = document.getElementById('simInfo');
const legendContainer = document.getElementById('legendContainer');
// Initial setup
let width, height;
const PADDING = 40;
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
width = rect.width;
height = rect.height;
draw();
}
// --- PHYSICS ENGINE ---
function initializeWave(type) {
// Reset arrays
u.fill(0);
u_prev.fill(0);
u_next.fill(0);
u_initial.fill(0);
timeStepCount = 0;
simInfo.textContent = `t = 0.00`;
const center = Math.floor(N / 2);
// Set u (current state at t=0)
if (type === 'triangle') {
// Pluck in the middle
const w = Math.floor(N / 4);
for (let i = 0; i < N; i++) {
if (i > center - w && i < center + w) {
u[i] = 1.0 - Math.abs(i - center) / w;
}
}
} else if (type === 'gaussian') {
const sigma = N / 20;
for (let i = 0; i < N; i++) {
u[i] = Math.exp(-Math.pow(i - center, 2) / (2 * sigma * sigma));
}
} else if (type === 'sine') {
for (let i = 0; i < N; i++) {
u[i] = Math.sin(Math.PI * i / (N - 1));
}
} else if (type === 'sine2') {
for (let i = 0; i < N; i++) {
u[i] = Math.sin(2 * Math.PI * i / (N - 1));
}
} else if (type === 'square') {
const w = Math.floor(N / 8);
for (let i = center - w; i < center + w; i++) {
u[i] = 1.0;
}
const k = 2; // smoothing kernel
for(let i=center-w-k; i<=center-w+k; i++) u[i] = 0.5;
for(let i=center+w-k; i<=center+w+k; i++) u[i] = 0.5;
} else if (type === 'noise') {
for (let i = 1; i < N - 1; i++) {
u[i] = (Math.random() - 0.5) * 0.2;
}
}
// Boundary conditions
u[0] = 0;
u[N-1] = 0;
// Copy to initial array for component visualization
for(let i=0; i<N; i++) u_initial[i] = u[i];
// Compute u_prev based on initial velocity = 0
for (let i = 1; i < N - 1; i++) {
u_prev[i] = u[i] + 0.5 * (C_VAL * C_VAL) * (u[i+1] - 2*u[i] + u[i-1]);
}
u_prev[0] = 0;
u_prev[N-1] = 0;
}
function solveStep() {
const C2 = C_VAL * C_VAL;
for (let i = 1; i < N - 1; i++) {
u_next[i] = 2 * u[i] - u_prev[i] + C2 * (u[i+1] - 2 * u[i] + u[i-1]);
}
u_next[0] = 0;
u_next[N-1] = 0;
let temp = u_prev;
u_prev = u;
u = u_next;
u_next = temp;
timeStepCount++;
}
// --- HELPER FOR REFLECTIONS ---
// Returns the value of the initial wave at index idx, considering odd boundary conditions
function getInitialValue(idx) {
const maxIdx = N - 1;
const period = 2 * maxIdx;
// Normalize index to handle periodicity
let normIdx = idx % period;
if (normIdx < 0) normIdx += period;
// Handle Odd Symmetry
// If in [0, L], return val. If in (L, 2L), return -val(reflected)
if (normIdx <= maxIdx) {
return u_initial[normIdx];
} else {
return -u_initial[period - normIdx];
}
}
// --- RENDERING ---
function draw() {
ctx.clearRect(0, 0, width, height);
// Draw axis
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PADDING, height / 2);
ctx.lineTo(width - PADDING, height / 2);
ctx.stroke();
const xScale = (width - 2 * PADDING) / (N - 1);
const yScale = 100; // Amplitude scale in pixels
const yOffset = height / 2;
// --- Draw Components (if enabled) ---
if (showComponents) {
// Since C_VAL = 1.0, the wave moves exactly 1 index per time step
const shift = timeStepCount;
// Right traveling wave (Green) -> f(x - ct) -> moves to larger indices
ctx.beginPath();
ctx.strokeStyle = '#28a745'; // Green
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]); // Dashed
for (let i = 0; i < N; i++) {
// Right component is 1/2 * phi(x - ct)
const val = 0.5 * getInitialValue(i - shift);
const x = PADDING + i * xScale;
const y = yOffset - val * yScale;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Left traveling wave (Orange) -> g(x + ct) -> moves to smaller indices
ctx.beginPath();
ctx.strokeStyle = '#fd7e14'; // Orange
ctx.lineWidth = 2;
//ctx.setLineDash([5, 5]); // Keep dashed
for (let i = 0; i < N; i++) {
// Left component is 1/2 * phi(x + ct)
const val = 0.5 * getInitialValue(i + shift);
const x = PADDING + i * xScale;
const y = yOffset - val * yScale;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.setLineDash([]); // Reset dash
}
// --- Draw Main String ---
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = '#007bff'; // Blue
for (let i = 0; i < N; i++) {
const x = PADDING + i * xScale;
const y = yOffset - u[i] * yScale;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Draw endpoints
ctx.fillStyle = '#dc3545';
ctx.beginPath();
ctx.arc(PADDING, yOffset, 4, 0, Math.PI * 2);
ctx.arc(width - PADDING, yOffset, 4, 0, Math.PI * 2);
ctx.fill();
}
function animate() {
if (!isRunning) return;
for(let k=0; k<speedMultiplier; k++) solveStep();
draw();
simInfo.textContent = `Time steps: ${timeStepCount}`;
animationId = requestAnimationFrame(animate);
}
// --- INTERACTIONS ---
startStopBtn.addEventListener('click', () => {
isRunning = !isRunning;
if (isRunning) {
startStopBtn.textContent = "Pause";
startStopBtn.classList.remove('btn-primary');
startStopBtn.classList.add('btn-danger');
animate();
} else {
startStopBtn.textContent = "Start";
startStopBtn.classList.remove('btn-danger');
startStopBtn.classList.add('btn-primary');
cancelAnimationFrame(animationId);
}
});
resetBtn.addEventListener('click', () => {
isRunning = false;
startStopBtn.textContent = "Start";
startStopBtn.classList.remove('btn-danger');
startStopBtn.classList.add('btn-primary');
cancelAnimationFrame(animationId);
initializeWave(shapeSelect.value);
draw();
});
shapeSelect.addEventListener('change', (e) => {
isRunning = false;
startStopBtn.textContent = "Start";
startStopBtn.classList.remove('btn-danger');
startStopBtn.classList.add('btn-primary');
cancelAnimationFrame(animationId);
initializeWave(e.target.value);
draw();
});
speedRange.addEventListener('input', (e) => {
speedMultiplier = parseInt(e.target.value);
});
componentsToggle.addEventListener('change', (e) => {
showComponents = e.target.checked;
legendContainer.style.display = showComponents ? 'flex' : 'none';
if (!isRunning) draw();
});
window.addEventListener('resize', () => {
resize();
});
// Initialize
resize();
initializeWave('triangle');
draw();
})();
</script>
</body>
</html>
```
## Intefeference of Waves
The interference of waves is a fascinating phenomenon that arises when two or more waves overlap in space and time. Depending on their relative phase and amplitude, the resulting interference can be constructive or destructive, leading to a variety of patterns and effects.
### Constructive Interference
Constructive interference occurs when two waves are in phase, meaning their peaks and troughs align. In this case, the amplitudes of the waves add up, resulting in a wave with a larger amplitude. This reinforcement of the waves leads to a constructive interference pattern, where the combined wave appears stronger than the individual waves.
### Destructive Interference
Destructive interference, on the other hand, arises when two waves are out of phase, with peaks aligning with troughs. In this scenario, the amplitudes of the waves cancel each other out, leading to a wave with a smaller amplitude or no wave at all. This destructive interference pattern results in regions of minimal or zero amplitude, where the waves effectively eliminate each other.
### Superposition Principle
The interference of waves is governed by the superposition principle, which states that the total displacement of a medium at any point and time is the sum of the displacements caused by each individual wave. This principle allows us to predict the resulting interference pattern by analyzing the properties of the individual waves.
### Visualization of Interference
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
A1 = 1.0 # Amplitude of wave 1
A2 = 0.8 # Amplitude of wave 2
k1 = 2.0 # Wave number of wave 1
k2 = 2.5 # Wave number of wave 2
omega1 = 1.0 # Angular frequency of wave 1
omega2 = 1.0 # Angular frequency of wave 2
phi1 = 0 # Phase of wave 1
phi2 = np.pi # Phase of wave 2
t=0
x = np.linspace(0, 8, 1000) # Range for x
# Wave functions
psi1 = A1 * np.sin(k1 * x + omega1 * t + phi1)
psi2 = A2 * np.sin(k2 * x + omega2 * t + phi2)
psi_total = psi1 + psi2
# Plot the wave functions
plt.figure(figsize=(12, 6))
plt.plot(x, psi1, label="$\\psi_1(x) = A_1 \\sin(k_1 x - \\omega_1 t + \\phi_1)$", color="blue")
plt.plot(x, psi2, label="$\\psi_2(x) = A_2 \\sin(k_2 x - \\omega_2 t + \\phi_2)$", color="red")
plt.plot(x, psi_total, label="$\\psi_{\\text{total}}(x) = \\psi_1(x) + \\psi_2(x)$", color="green")
# Add auxiliary lines
plt.axhline(y=0, color='gray', linestyle='--', linewidth=0.7, alpha=0.5) # Horizontal axis
# Configure axes
plt.xlabel("Position $x$", fontsize=14)
plt.ylabel("Wave function $\\psi(x)$", fontsize=14)
# Add grid
plt.grid(True, linestyle="--", alpha=0.7)
# Display the plot
plt.legend()
plt.show()
```
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wave Interference & Superposition</title>
<style>
/* Scoped styles for the widget to prevent global conflicts */
.interference-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 1000px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.interference-app * {
box-sizing: border-box;
}
.app-header {
text-align: center;
margin-bottom: 20px;
}
.app-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.6rem;
}
.app-header p {
margin: 0;
color: #666;
font-size: 0.95rem;
}
/* Canvas Area */
.canvas-wrapper {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
box-shadow: inset 0 0 10px rgba(0,0,0,0.02);
}
#interferenceCanvas {
display: block;
width: 100%;
height: 400px;
cursor: crosshair;
}
/* Legend */
.legend-bar {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
font-size: 0.9rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-line {
width: 20px;
height: 3px;
border-radius: 2px;
}
/* Controls Layout */
.controls-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background-color: #fff;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.wave-panel {
padding: 10px;
border-radius: 4px;
border-left: 4px solid transparent;
}
.wave-panel.blue { border-left-color: #007bff; background-color: rgba(0, 123, 255, 0.03); }
.wave-panel.red { border-left-color: #dc3545; background-color: rgba(220, 53, 69, 0.03); }
.wave-panel h3 {
margin-top: 0;
font-size: 1.1rem;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.control-row {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.control-row label {
font-size: 0.85rem;
font-weight: 600;
color: #555;
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.val-display {
font-family: monospace;
color: #007bff;
}
.red .val-display { color: #dc3545; }
/* Sliders */
.interference-app input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
.interference-app input[type=range]:focus { outline: none; }
/* Thumb styling */
.interference-app input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 14px; width: 14px; border-radius: 50%;
background: #6c757d; cursor: pointer; margin-top: -5px;
}
.blue input[type=range]::-webkit-slider-thumb { background: #007bff; }
.red input[type=range]::-webkit-slider-thumb { background: #dc3545; }
.interference-app input[type=range]::-webkit-slider-runnable-track {
width: 100%; height: 4px; cursor: pointer;
background: #dee2e6; border-radius: 2px;
}
/* Bottom Toolbar (Presets & Playback) */
.bottom-toolbar {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.preset-group {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-outline {
background-color: transparent;
border: 1px solid #ced4da;
color: #495057;
}
.btn-outline:hover { background-color: #e9ecef; border-color: #adb5bd; }
.btn-primary {
background-color: #28a745;
color: white;
min-width: 100px;
}
.btn-primary:hover { background-color: #218838; }
.btn-paused {
background-color: #dc3545;
}
.btn-paused:hover { background-color: #c82333; }
.checkbox-label {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
cursor: pointer;
}
@media (max-width: 700px) {
.controls-container { grid-template-columns: 1fr; }
.bottom-toolbar { flex-direction: column; align-items: stretch; }
.preset-group { justify-content: center; }
}
</style>
</head>
<body>
<div class="interference-app">
<div class="app-header">
<h2>Superposition Principle</h2>
<p>Visualizing the sum of two harmonic waves</p>
</div>
<div class="canvas-wrapper">
<canvas id="interferenceCanvas"></canvas>
</div>
<div class="legend-bar">
<div class="legend-item">
<div class="legend-line" style="background: #007bff; height: 2px;"></div>
<span>Wave 1 (ψ<sub>1</sub>)</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #dc3545; height: 2px;"></div>
<span>Wave 2 (ψ<sub>2</sub>)</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #28a745; height: 4px;"></div>
<span><strong>Sum (ψ<sub>total</sub>)</strong></span>
</div>
</div>
<div class="controls-container">
<!-- Wave 1 Controls -->
<div class="wave-panel blue">
<h3>Wave 1 (Blue)
<label class="checkbox-label" style="font-size: 0.8rem; font-weight: normal;">
<input type="checkbox" id="showW1" checked> Visible
</label>
</h3>
<div class="control-row">
<label>Amplitude (A<sub>1</sub>): <span id="val-a1" class="val-display">1.0</span></label>
<input type="range" id="slider-a1" min="0" max="1.5" step="0.1" value="1.0">
</div>
<div class="control-row">
<label>Wavelength (λ<sub>1</sub>): <span id="val-l1" class="val-display">4.0</span></label>
<input type="range" id="slider-l1" min="1.0" max="10.0" step="0.1" value="4.0">
</div>
<div class="control-row">
<label>Velocity (v<sub>1</sub>): <span id="val-v1" class="val-display">1.0</span></label>
<input type="range" id="slider-v1" min="-2.0" max="2.0" step="0.1" value="1.0">
</div>
<div class="control-row">
<label>Phase Shift (φ<sub>1</sub>): <span id="val-p1" class="val-display">0.00</span> π</label>
<input type="range" id="slider-p1" min="0" max="2" step="0.05" value="0">
</div>
</div>
<!-- Wave 2 Controls -->
<div class="wave-panel red">
<h3>Wave 2 (Red)
<label class="checkbox-label" style="font-size: 0.8rem; font-weight: normal;">
<input type="checkbox" id="showW2" checked> Visible
</label>
</h3>
<div class="control-row">
<label>Amplitude (A<sub>2</sub>): <span id="val-a2" class="val-display">1.0</span></label>
<input type="range" id="slider-a2" min="0" max="1.5" step="0.1" value="1.0">
</div>
<div class="control-row">
<label>Wavelength (λ<sub>2</sub>): <span id="val-l2" class="val-display">4.0</span></label>
<input type="range" id="slider-l2" min="1.0" max="10.0" step="0.1" value="4.0">
</div>
<div class="control-row">
<label>Velocity (v<sub>2</sub>): <span id="val-v2" class="val-display">-1.0</span></label>
<input type="range" id="slider-v2" min="-2.0" max="2.0" step="0.1" value="-1.0">
</div>
<div class="control-row">
<label>Phase Shift (φ<sub>2</sub>): <span id="val-p2" class="val-display">0.00</span> π</label>
<input type="range" id="slider-p2" min="0" max="2" step="0.05" value="0">
</div>
</div>
<!-- Bottom Toolbar -->
<div class="bottom-toolbar">
<div class="preset-group">
<button class="btn btn-outline" onclick="setPreset('standing')">Standing Wave</button>
<button class="btn btn-outline" onclick="setPreset('constructive')">Constructive</button>
<button class="btn btn-outline" onclick="setPreset('destructive')">Destructive</button>
<button class="btn btn-outline" onclick="setPreset('beats')">Beats</button>
</div>
<button id="playPauseBtn" class="btn btn-primary">Pause</button>
</div>
</div>
</div>
<script>
// Scope to prevent global namespace pollution
(function() {
const canvas = document.getElementById('interferenceCanvas');
const ctx = canvas.getContext('2d');
// Params
const params = {
w1: { A: 1.0, lambda: 4.0, v: 1.0, phi: 0.0, visible: true },
w2: { A: 1.0, lambda: 4.0, v: -1.0, phi: 0.0, visible: true }
};
// State
let isRunning = true;
let time = 0;
let lastTime = performance.now();
let animationId;
// Scale factors
let width, height;
const scaleX = 60; // pixels per meter
const scaleY = 60; // pixels per unit amplitude
const yOffset = 0.5; // normalized (0.5 = center)
// UI References
const ui = {
w1: {
A: document.getElementById('slider-a1'),
l: document.getElementById('slider-l1'),
v: document.getElementById('slider-v1'),
p: document.getElementById('slider-p1'),
vis: document.getElementById('showW1'),
valA: document.getElementById('val-a1'),
valL: document.getElementById('val-l1'),
valV: document.getElementById('val-v1'),
valP: document.getElementById('val-p1')
},
w2: {
A: document.getElementById('slider-a2'),
l: document.getElementById('slider-l2'),
v: document.getElementById('slider-v2'),
p: document.getElementById('slider-p2'),
vis: document.getElementById('showW2'),
valA: document.getElementById('val-a2'),
valL: document.getElementById('val-l2'),
valV: document.getElementById('val-v2'),
valP: document.getElementById('val-p2')
},
btn: document.getElementById('playPauseBtn')
};
// --- Core Logic ---
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
width = rect.width;
height = rect.height;
draw();
}
function getWaveY(waveParams, x_meters, t) {
// y = A * sin(k(x - vt) + phi)
// k = 2*pi / lambda
const k = (2 * Math.PI) / waveParams.lambda;
// Phase phi is in units of PI from slider
const phase = waveParams.phi * Math.PI;
return waveParams.A * Math.sin(k * (x_meters - waveParams.v * t) + phase);
}
function draw() {
ctx.clearRect(0, 0, width, height);
// Draw Axes
const centerY = height * yOffset;
ctx.beginPath();
ctx.moveTo(0, centerY);
ctx.lineTo(width, centerY);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();
// Grid (Vertical)
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
const maxMeters = width / scaleX;
for(let m = 0; m <= maxMeters; m++) {
const px = m * scaleX;
ctx.beginPath();
ctx.moveTo(px, 0);
ctx.lineTo(px, height);
ctx.stroke();
// Axis Label
ctx.fillStyle = '#999';
ctx.font = '10px sans-serif';
ctx.fillText(m, px + 2, height - 5);
}
// --- Wave Calculation Loop ---
// To optimize, we'll store points
const points1 = [];
const points2 = [];
const pointsSum = [];
// Resolution: 1 point per pixel
for (let px = 0; px < width; px++) {
const x_meters = px / scaleX;
const y1 = getWaveY(params.w1, x_meters, time);
const y2 = getWaveY(params.w2, x_meters, time);
const ySum = y1 + y2;
points1.push(y1);
points2.push(y2);
pointsSum.push(ySum);
}
// Helper to draw a path
function drawPath(points, color, width, dash = []) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.setLineDash(dash);
for (let px = 0; px < points.length; px++) {
// Invert Y for canvas
const py = centerY - points[px] * scaleY;
if (px === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.stroke();
ctx.setLineDash([]);
}
// Draw Wave 1 (Blue)
if (params.w1.visible) {
drawPath(points1, '#007bff', 2);
}
// Draw Wave 2 (Red)
if (params.w2.visible) {
drawPath(points2, '#dc3545', 2);
}
// Draw Sum (Green) - Always visible
drawPath(pointsSum, '#28a745', 4); // Thicker line
// Legend inside canvas (Optional, removed since we have external legend)
}
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
if (isRunning) {
time += dt;
}
draw();
animationId = requestAnimationFrame(animate);
}
// --- Interaction ---
function updateParamsFromUI() {
// Wave 1
params.w1.A = parseFloat(ui.w1.A.value);
params.w1.lambda = parseFloat(ui.w1.l.value);
params.w1.v = parseFloat(ui.w1.v.value);
params.w1.phi = parseFloat(ui.w1.p.value);
params.w1.visible = ui.w1.vis.checked;
ui.w1.valA.textContent = params.w1.A.toFixed(1);
ui.w1.valL.textContent = params.w1.lambda.toFixed(1);
ui.w1.valV.textContent = params.w1.v.toFixed(1);
ui.w1.valP.textContent = params.w1.phi.toFixed(2);
// Wave 2
params.w2.A = parseFloat(ui.w2.A.value);
params.w2.lambda = parseFloat(ui.w2.l.value);
params.w2.v = parseFloat(ui.w2.v.value);
params.w2.phi = parseFloat(ui.w2.p.value);
params.w2.visible = ui.w2.vis.checked;
ui.w2.valA.textContent = params.w2.A.toFixed(1);
ui.w2.valL.textContent = params.w2.lambda.toFixed(1);
ui.w2.valV.textContent = params.w2.v.toFixed(1);
ui.w2.valP.textContent = params.w2.phi.toFixed(2);
if (!isRunning) draw();
}
// --- Presets ---
window.setPreset = function(type) {
if (type === 'standing') {
// Equal amplitude, equal wavelength, opposite velocities
setUIValues(1.0, 4.0, 1.0, 0.0, // W1
1.0, 4.0, -1.0, 0.0); // W2
// Ensure visible
ui.w1.vis.checked = true;
ui.w2.vis.checked = true;
} else if (type === 'constructive') {
// Same direction, phase 0
setUIValues(1.0, 4.0, 1.0, 0.0,
1.0, 4.0, 1.0, 0.0);
} else if (type === 'destructive') {
// Same direction, phase PI (1.0 in slider units)
setUIValues(1.0, 4.0, 1.0, 0.0,
1.0, 4.0, 1.0, 1.0);
} else if (type === 'beats') {
// Slightly different wavelengths, same speed
setUIValues(1.0, 4.0, 2.0, 0.0,
1.0, 3.5, 2.0, 0.0);
}
updateParamsFromUI();
// Auto start if paused? Optional.
if (!isRunning) {
togglePlay();
}
};
function setUIValues(a1, l1, v1, p1, a2, l2, v2, p2) {
ui.w1.A.value = a1; ui.w1.l.value = l1; ui.w1.v.value = v1; ui.w1.p.value = p1;
ui.w2.A.value = a2; ui.w2.l.value = l2; ui.w2.v.value = v2; ui.w2.p.value = p2;
}
function togglePlay() {
isRunning = !isRunning;
if (isRunning) {
ui.btn.textContent = "Pause";
ui.btn.classList.remove('btn-paused');
ui.btn.classList.add('btn-primary');
lastTime = performance.now();
} else {
ui.btn.textContent = "Resume";
ui.btn.classList.remove('btn-primary');
ui.btn.classList.add('btn-paused');
}
}
// --- Event Listeners ---
const inputs = [
ui.w1.A, ui.w1.l, ui.w1.v, ui.w1.p, ui.w1.vis,
ui.w2.A, ui.w2.l, ui.w2.v, ui.w2.p, ui.w2.vis
];
inputs.forEach(input => {
input.addEventListener('input', updateParamsFromUI);
input.addEventListener('change', updateParamsFromUI);
});
ui.btn.addEventListener('click', togglePlay);
window.addEventListener('resize', () => {
resize();
});
// Initialize
resize();
updateParamsFromUI();
setPreset('standing'); // Default start
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
```
## Huygens–Fresnel principle
The Huygens–Fresnel principle is a fundamental concept in wave optics that describes how every point on a wavefront can be considered as a source of secondary spherical waves. These secondary waves combine to form the wavefront at a later time, allowing us to predict the propagation of waves through various media and obstacles.
### Refraction
Refraction is a common phenomenon where waves change direction as they pass from one medium to another. The Huygens–Fresnel principle provides a simple explanation for refraction, showing how the secondary waves generated at each point on the wavefront propagate through the new medium, leading to a change in the wave's direction.

#### Snell's Law
Snell's Law is a key result derived from the Huygens–Fresnel principle, describing the relationship between the angles of incidence and refraction for waves passing through different media. The law states that the ratio of the sines of the angles is equal to the ratio of the wave speeds in the two media:
$$
\frac{\sin(\theta_1)}{\sin(\theta_2)} = \frac{v_1}{v_2},
$$
where $\theta_1$ and $\theta_2$ are the angles of incidence and refraction, respectively, and $v_1$ and $v_2$ are the wave speeds in the two media.
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Huygens-Fresnel Principle: Refraction</title>
<style>
.huygens-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.huygens-app * {
box-sizing: border-box;
}
.app-header {
text-align: center;
margin-bottom: 20px;
}
.app-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.6rem;
}
.canvas-wrapper {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
}
#huygensCanvas {
display: block;
width: 100%;
height: 450px;
}
.controls-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background-color: #fff;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-row label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
}
.val-display {
font-family: monospace;
color: #007bff;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
.buttons-row {
grid-column: 1 / -1;
display: flex;
gap: 10px;
justify-content: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.btn {
padding: 8px 20px;
border: none;
border-radius: 4px;
font-size: 0.95rem;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover { background-color: #0056b3; }
.btn-reset {
background-color: #6c757d;
color: white;
}
.btn-reset:hover { background-color: #5a6268; }
.legend {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 10px;
font-size: 0.85rem;
flex-wrap: wrap;
}
.legend-item { display: flex; align-items: center; gap: 5px; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.line { width: 20px; height: 3px; display: inline-block; }
.alert-box {
position: absolute;
top: 10px;
right: 10px;
background: rgba(220, 53, 69, 0.9);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.85rem;
display: none;
}
@media (max-width: 600px) {
.controls-container { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="huygens-app">
<div class="app-header">
<h2>Huygens-Fresnel Refraction</h2>
<p>Wavelets construction at the interface</p>
</div>
<div class="legend">
<div class="legend-item">
<span class="line" style="background: #007bff;"></span> Incident Wave
</div>
<div class="legend-item">
<span class="dot" style="background: #ffc107;"></span> Secondary Sources
</div>
<div class="legend-item">
<span class="line" style="border: 1px solid #28a745; height: 10px; width:10px; border-radius: 50%;"></span> Wavelets
</div>
<div class="legend-item">
<span class="line" style="background: #dc3545;"></span> Refracted Wavefront
</div>
</div>
<div class="canvas-wrapper">
<canvas id="huygensCanvas"></canvas>
<div id="tirAlert" class="alert-box">Total Internal Reflection!</div>
</div>
<div class="controls-container">
<div class="control-group">
<div class="control-row">
<label>Medium 1 Index (n<sub>1</sub>)</label>
<span id="val-n1" class="val-display">1.0</span>
</div>
<input type="range" id="n1" min="1.0" max="2.5" step="0.1" value="1.0">
<div class="control-row">
<label>Medium 2 Index (n<sub>2</sub>)</label>
<span id="val-n2" class="val-display">1.5</span>
</div>
<input type="range" id="n2" min="1.0" max="2.5" step="0.1" value="1.5">
<div style="font-size: 0.8rem; color: #666; margin-top: 5px;">
Velocity ratio (v<sub>1</sub>/v<sub>2</sub>) = <span id="val-ratio">1.50</span>
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Incident Angle (θ<sub>1</sub>)</label>
<span id="val-angle" class="val-display">45°</span>
</div>
<input type="range" id="angle" min="0" max="80" step="1" value="45">
<div class="control-row">
<label>Refracted Angle (θ<sub>2</sub>)</label>
<span id="val-angle2" class="val-display">--</span>
</div>
<div class="control-row">
<label>Animation Speed</label>
</div>
<input type="range" id="speed" min="0.1" max="2.0" step="0.1" value="0.5">
</div>
<div class="buttons-row">
<button id="btn-play" class="btn btn-primary">Pause</button>
<button id="btn-reset" class="btn btn-reset">Reset Wave</button>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('huygensCanvas');
const ctx = canvas.getContext('2d');
const tirAlert = document.getElementById('tirAlert');
// UI Refs
const ui = {
n1: document.getElementById('n1'),
n2: document.getElementById('n2'),
angle: document.getElementById('angle'),
speed: document.getElementById('speed'),
vn1: document.getElementById('val-n1'),
vn2: document.getElementById('val-n2'),
vang: document.getElementById('val-angle'),
vang2: document.getElementById('val-angle2'),
vratio: document.getElementById('val-ratio'),
btnPlay: document.getElementById('btn-play'),
btnReset: document.getElementById('btn-reset')
};
// State
let state = {
n1: 1.0,
n2: 1.5,
theta1: 45 * Math.PI / 180, // radians
speed: 0.5,
time: 0,
running: true
};
// Constants
const BASE_VELOCITY = 100; // pixels per second (base c)
let width, height;
let sources = [];
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
width = rect.width;
height = rect.height;
initSources();
}
function initSources() {
sources = [];
const numSources = 12;
const step = width / (numSources + 2); // padding
for(let i=1; i<=numSources; i++) {
sources.push({ x: i * step, y: height/2, hitTime: null });
}
}
function updatePhysics() {
// Snell's Law: n1 * sin(t1) = n2 * sin(t2)
const sinT2 = (state.n1 / state.n2) * Math.sin(state.theta1);
let theta2 = 0;
let isTIR = false;
if (Math.abs(sinT2) > 1.0) {
isTIR = true;
theta2 = null;
} else {
theta2 = Math.asin(sinT2);
}
// Velocities
const v1 = BASE_VELOCITY / state.n1;
const v2 = BASE_VELOCITY / state.n2;
return { v1, v2, theta2, isTIR };
}
function resetWave() {
state.time = -2; // Start slightly before
sources.forEach(s => s.hitTime = null);
}
function draw() {
const { v1, v2, theta2, isTIR } = updatePhysics();
// Update UI
ui.vang2.textContent = isTIR ? "TIR" : (theta2 * 180 / Math.PI).toFixed(1) + "°";
ui.vratio.textContent = (state.n2 / state.n1).toFixed(2);
tirAlert.style.display = isTIR ? 'block' : 'none';
// Clear
ctx.clearRect(0, 0, width, height);
// Draw Media Backgrounds
const midY = height / 2;
// Medium 1 (Top)
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, midY);
// Medium 2 (Bottom)
ctx.fillStyle = "#f1f3f5"; // Slight gray
ctx.fillRect(0, midY, width, midY);
// Draw Interface Line
ctx.beginPath();
ctx.moveTo(0, midY);
ctx.lineTo(width, midY);
ctx.strokeStyle = "#adb5bd";
ctx.lineWidth = 2;
ctx.stroke();
// Draw Normal (Dashed)
ctx.beginPath();
ctx.moveTo(width/2, 20);
ctx.lineTo(width/2, height - 20);
ctx.setLineDash([5, 5]);
ctx.strokeStyle = "#999";
ctx.lineWidth = 1;
ctx.stroke();
ctx.setLineDash([]);
// --- Draw Rays ---
const centerX = width/2;
const rayLen = 300;
// Incident Ray (Medium 1)
// Starts top left usually if theta > 0
const ix = centerX - rayLen * Math.sin(state.theta1);
const iy = midY - rayLen * Math.cos(state.theta1);
ctx.beginPath();
ctx.moveTo(ix, iy);
ctx.lineTo(centerX, midY);
ctx.strokeStyle = "rgba(0, 123, 255, 0.4)";
ctx.lineWidth = 3;
ctx.stroke();
// Refracted Ray (Medium 2)
if (!isTIR) {
const rx = centerX + rayLen * Math.sin(theta2);
const ry = midY + rayLen * Math.cos(theta2);
ctx.beginPath();
ctx.moveTo(centerX, midY);
ctx.lineTo(rx, ry);
ctx.strokeStyle = "rgba(0, 123, 255, 0.4)";
ctx.stroke();
}
// --- Wavefront Simulation ---
const distance = state.time * v1; // Distance of wavefront from center along ray direction
// Calculate hit times for sources
// The wave hits the interface at different times.
// Based on geometry, time delay relative to center is: x_rel * sin(theta) / v1
sources.forEach(s => {
const proj = (s.x - centerX) * Math.sin(state.theta1);
s.hitTime = (proj / v1);
});
// 1. Draw Incident Wavefront (The one that triggers Huygens)
if (state.time < 1.5) { // Only draw if relevant
// Calculation of wavefront position
// The wavefront is perpendicular to the ray direction.
// Its position along the ray is given by 'distance' (v1 * t).
// Center of wavefront (intersection with ray):
// Ray goes from Top-Left to Center. Direction vector D = (sin t1, cos t1).
// At t=0, it's at Center. At t<0, it's at Center + t*v1*D = Center - |dist|*D.
// Or simply: P = Center + (v1*t)*D. Since t is signed, this works.
const rX = centerX + distance * Math.sin(state.theta1); // Changed - to + to fix direction
const rY = midY + distance * Math.cos(state.theta1); // Changed - to + to fix direction
// Note: since 't' starts negative, (v1*t) is negative.
// We want to be at Top-Left.
// CenterX + (negative) * (positive_sin) = Left. Correct.
// MidY + (negative) * (positive_cos) = Top. Correct.
// The previous version used '-' which flipped it to Bottom-Right.
// Perpendicular vector (-cos t1, sin t1)
const pX = -Math.cos(state.theta1);
const pY = Math.sin(state.theta1);
const L = 600;
ctx.beginPath();
ctx.moveTo(rX - pX*L, rY - pY*L);
ctx.lineTo(rX + pX*L, rY + pY*L);
ctx.strokeStyle = "#007bff";
ctx.lineWidth = 2;
// Clip to top half
ctx.save();
ctx.rect(0,0,width,midY);
ctx.clip();
ctx.stroke();
ctx.restore();
}
// 2. Draw Secondary Sources and Wavelets
sources.forEach(s => {
// Draw source dot
ctx.beginPath();
ctx.arc(s.x, s.y, 4, 0, Math.PI*2);
ctx.fillStyle = "#ffc107";
ctx.fill();
// Check if wave has hit this source
if (state.time > s.hitTime) {
const dt = state.time - s.hitTime;
const radius = v2 * dt;
// Draw Wavelet (Semi-circle in Medium 2)
if (!isTIR && radius > 0) {
ctx.beginPath();
ctx.arc(s.x, s.y, radius, 0, Math.PI, false);
ctx.strokeStyle = "rgba(40, 167, 69, 0.6)"; // Greenish
ctx.lineWidth = 1;
ctx.stroke();
}
}
});
// 3. Draw Refracted Wavefront (Envelope)
if (!isTIR && state.time > -0.5) {
const dist2 = v2 * state.time;
// For refracted wave (t > 0), it moves away from center into Medium 2.
// Direction D2 = (sin t2, cos t2).
// Position P2 = Center + (v2*t)*D2.
const rX2 = centerX + dist2 * Math.sin(theta2);
const rY2 = midY + dist2 * Math.cos(theta2);
// Perpendicular vector (-cos t2, sin t2)
const pX2 = -Math.cos(theta2);
const pY2 = Math.sin(theta2);
const L2 = 600;
ctx.beginPath();
ctx.moveTo(rX2 - pX2*L2, rY2 - pY2*L2);
ctx.lineTo(rX2 + pX2*L2, rY2 + pY2*L2);
ctx.strokeStyle = "#dc3545"; // Red
ctx.lineWidth = 3;
// Clip to bottom half
ctx.save();
ctx.rect(0, midY, width, height - midY);
ctx.clip();
ctx.stroke();
ctx.restore();
}
}
function animate() {
if (state.running) {
state.time += 0.016 * state.speed; // approx 60fps
// Auto reset if too far
if (state.time > 4.0) {
state.time = -2.0;
}
}
draw();
requestAnimationFrame(animate);
}
// Interaction
function updateFromUI() {
state.n1 = parseFloat(ui.n1.value);
state.n2 = parseFloat(ui.n2.value);
state.theta1 = parseFloat(ui.angle.value) * Math.PI / 180;
state.speed = parseFloat(ui.speed.value);
ui.vn1.textContent = state.n1.toFixed(1);
ui.vn2.textContent = state.n2.toFixed(1);
ui.vang.textContent = ui.angle.value + "°";
if (!state.running) draw();
}
[ui.n1, ui.n2, ui.angle, ui.speed].forEach(el => {
el.addEventListener('input', updateFromUI);
});
ui.btnPlay.addEventListener('click', () => {
state.running = !state.running;
ui.btnPlay.textContent = state.running ? "Pause" : "Resume";
});
ui.btnReset.addEventListener('click', () => {
resetWave();
if (!state.running) draw();
});
window.addEventListener('resize', resize);
// Init
resize();
resetWave();
updateFromUI();
animate();
})();
</script>
</body>
</html>
```
### Diffraction
Diffraction is another key concept explained by the Huygens–Fresnel principle, where waves bend around obstacles or pass through narrow openings. By considering each point on the wavefront as a source of secondary waves, we can predict how the diffracted waves will propagate and create interference patterns.

{width=50%}
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diffraction & Interference Explorer</title>
<style>
/* Scoped styles */
.diffraction-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 1000px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.diffraction-app * {
box-sizing: border-box;
}
.app-header {
text-align: center;
margin-bottom: 20px;
}
.app-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.6rem;
}
/* Tabs */
.tabs {
display: flex;
justify-content: center;
margin-bottom: 15px;
gap: 10px;
}
.tab-btn {
padding: 10px 20px;
background: #e9ecef;
border: none;
border-radius: 20px;
cursor: pointer;
font-weight: 600;
color: #495057;
transition: all 0.2s;
font-size: 1rem;
}
.tab-btn.active {
background: #007bff;
color: white;
box-shadow: 0 2px 4px rgba(0,123,255,0.3);
}
/* Canvas Area */
.canvas-container {
width: 100%;
background-color: #000;
border: 1px solid #333;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
}
#diffCanvas {
background-color: #111;
cursor: crosshair;
width: 100%;
height: 400px;
}
/* Legend overlay */
.legend-overlay {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 8px;
border-radius: 4px;
font-size: 0.8rem;
pointer-events: none;
}
.legend-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.dot { width: 8px; height: 8px; border-radius: 50%; }
.line { width: 20px; height: 2px; }
/* Controls */
.controls-panel {
background-color: #fff;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
display: flex;
justify-content: space-between;
}
.val-display {
font-family: monospace;
color: #007bff;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
</style>
</head>
<body>
<div class="diffraction-app">
<div class="app-header">
<h2>Huygens-Fresnel: Diffraction & Interference</h2>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="switchMode('single')">Single Slit Diffraction</button>
<button class="tab-btn" onclick="switchMode('double')">Double Slit Interference</button>
</div>
<div class="canvas-container">
<canvas id="diffCanvas"></canvas>
<div class="legend-overlay">
<div class="legend-row"><span class="dot" style="background: #ffd700;"></span> <span>Secondary Sources</span></div>
<div class="legend-row"><span class="line" style="border-top: 2px dashed #32cd32;"></span> <span>Huygens Wavelets</span></div>
<div class="legend-row"><span class="line" style="background: #ff4d4d; height: 3px;"></span> <span>Mean Intensity</span></div>
</div>
</div>
<div class="controls-panel">
<div class="control-group">
<div class="control-label">
<label>Wavelength (λ)</label>
<span id="val-lambda" class="val-display">--</span>
</div>
<input type="range" id="slider-lambda" min="20" max="60" step="1" value="35">
</div>
<div class="control-group">
<div class="control-label">
<label>Slit Width (a)</label>
<span id="val-width" class="val-display">--</span>
</div>
<input type="range" id="slider-width" min="10" max="100" step="5" value="60">
</div>
<div class="control-group" id="group-separation" style="display: none; opacity: 0.5;">
<div class="control-label">
<label>Slit Separation (d)</label>
<span id="val-sep" class="val-display">--</span>
</div>
<input type="range" id="slider-sep" min="40" max="150" step="5" value="80" disabled>
</div>
<div class="control-group">
<div class="control-label">
<label>Simulation Quality</label>
</div>
<label style="font-size: 0.85rem; cursor: pointer;">
<input type="checkbox" id="chk-wavelets" checked> Show Huygens Arcs
</label>
<label style="font-size: 0.85rem; cursor: pointer;">
<input type="checkbox" id="chk-field" checked> Show Field Map
</label>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('diffCanvas');
const ctx = canvas.getContext('2d', { alpha: false }); // Optimize for no transparency
// UI Elements
const sliderLambda = document.getElementById('slider-lambda');
const sliderWidth = document.getElementById('slider-width');
const sliderSep = document.getElementById('slider-sep');
const groupSep = document.getElementById('group-separation');
const valLambda = document.getElementById('val-lambda');
const valWidth = document.getElementById('val-width');
const valSep = document.getElementById('val-sep');
const chkWavelets = document.getElementById('chk-wavelets');
const chkField = document.getElementById('chk-field');
const tabs = document.querySelectorAll('.tab-btn');
// State
let mode = 'single'; // 'single' or 'double'
let time = 0;
let sources = [];
let animationId;
// Physics Parameters
let lambda = 35;
let slitWidth = 60;
let separation = 80;
let speed = 2.0;
// Configuration
const GRAPH_WIDTH = 150; // Width of the intensity graph panel on the right
// Render Scale (Optimization)
let width, height;
let simWidth; // Width of the wave simulation area
let imageData;
let fieldBuf; // Buffer for calculated field values
function init() {
window.switchMode = function(newMode) {
mode = newMode;
tabs.forEach(t => t.classList.remove('active'));
if(mode === 'single') {
tabs[0].classList.add('active');
groupSep.style.display = 'none';
groupSep.style.opacity = '0.3';
sliderSep.disabled = true;
} else {
tabs[1].classList.add('active');
groupSep.style.display = 'flex';
groupSep.style.opacity = '1';
sliderSep.disabled = false;
}
updateSources();
};
resize();
updateSources();
animate();
}
function resize() {
const rect = canvas.getBoundingClientRect();
// Ensure valid dimensions to prevent IndexSizeError if layout isn't ready
let newWidth = Math.floor(rect.width);
let newHeight = Math.floor(rect.height);
// Fallback dimensions
if (newWidth === 0 || newHeight === 0) {
newWidth = 800;
newHeight = 400;
}
canvas.width = newWidth;
canvas.height = newHeight;
width = newWidth;
height = newHeight;
simWidth = width - GRAPH_WIDTH;
// Re-create buffers with valid dimensions
imageData = ctx.createImageData(width, height);
fieldBuf = new Float32Array(height);
updateSources();
}
function updateSources() {
sources = [];
const barrierX = simWidth * 0.25; // Barrier position
const centerY = height / 2;
// Density of Huygens sources (points per pixel)
const density = 0.15; // 1 source every ~6-7 pixels
if (mode === 'single') {
const numPoints = Math.max(3, Math.floor(slitWidth * density));
const startY = centerY - slitWidth/2;
for(let i=0; i<=numPoints; i++) {
const y = startY + (i / numPoints) * slitWidth;
sources.push({ x: barrierX, y: y });
}
} else {
// Double Slit
const numPoints = Math.max(2, Math.floor(slitWidth * density));
// Top slit
let startY = centerY - separation/2 - slitWidth/2;
for(let i=0; i<=numPoints; i++) {
const y = startY + (i / numPoints) * slitWidth;
sources.push({ x: barrierX, y: y });
}
// Bottom slit
startY = centerY + separation/2 - slitWidth/2;
for(let i=0; i<=numPoints; i++) {
const y = startY + (i / numPoints) * slitWidth;
sources.push({ x: barrierX, y: y });
}
}
}
function calculateField() {
// Guard clause if initialization failed
if (!imageData) return;
const k = (2 * Math.PI) / lambda;
const barrierX = simWidth * 0.25;
// Phase for animation: k*r - omega*t
const phaseT = (2 * Math.PI * time * speed) / lambda;
const ptr = imageData.data;
const showField = chkField.checked;
const sx = new Float32Array(sources.length);
const sy = new Float32Array(sources.length);
for(let s=0; s<sources.length; s++) { sx[s] = sources[s].x; sy[s] = sources[s].y; }
const numSources = sources.length;
// 1. Calculate Field Map (Time Dependent)
let idx = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// --- GRAPH AREA (RIGHT SIDE) ---
if (x >= simWidth) {
ptr[idx] = 0; idx += 4; continue;
}
// --- SIMULATION AREA (LEFT SIDE) ---
let val = 0;
if (x < barrierX) {
// Plane wave
val = Math.sin((2 * Math.PI * x) / lambda - phaseT);
} else {
// Interference
if (showField) {
for (let s = 0; s < numSources; s++) {
const dx = x - sx[s];
const dy = y - sy[s];
const r = Math.sqrt(dx*dx + dy*dy);
const amp = 10.0 / (Math.sqrt(r) + 1.0);
const phase = k * r - phaseT;
val += amp * Math.sin(phase);
}
val = val / Math.sqrt(numSources);
}
}
// Draw
if (showField) {
let intensity = 128 + val * 80;
if (intensity < 0) intensity = 0;
if (intensity > 255) intensity = 255;
ptr[idx] = intensity; ptr[idx+1] = intensity; ptr[idx+2] = intensity; ptr[idx+3] = 255;
} else {
ptr[idx] = 20; ptr[idx+1] = 20; ptr[idx+2] = 20; ptr[idx+3] = 255;
}
idx += 4;
}
}
ctx.putImageData(imageData, 0, 0);
// 2. Calculate Screen Intensity (Time Averaged / Static)
// We calculate this separately for the screen line (x = simWidth)
// Intensity ~ Amplitude^2. E_total = sum(A_i * exp(i*k*r_i))
// We sum phasors components: sum(cos(kr)) and sum(sin(kr))
const screenX = simWidth - 1;
for (let y = 0; y < height; y++) {
let sumSin = 0;
let sumCos = 0;
for (let s = 0; s < numSources; s++) {
const dx = screenX - sx[s];
const dy = y - sy[s];
const r = Math.sqrt(dx*dx + dy*dy);
const amp = 10.0 / (Math.sqrt(r) + 1.0);
const kr = k * r; // Phase without time component
sumSin += amp * Math.sin(kr);
sumCos += amp * Math.cos(kr);
}
// Average Intensity ~ Amplitude Squared
// Amplitude = sqrt(sumSin^2 + sumCos^2)
// We divide by numSources to normalize to similar scale as field map
const amplitudeSq = (sumSin*sumSin + sumCos*sumCos) / numSources;
// Store Amplitude (envelope) in buffer so we can square it later for Intensity graph
// or just store the value we want to plot.
// Let's store amplitude, so graph calculates Amplitude^2 = Intensity
fieldBuf[y] = Math.sqrt(amplitudeSq);
}
}
function drawOverlay() {
const barrierX = simWidth * 0.25;
const centerY = height / 2;
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, simWidth, height);
ctx.clip();
// Draw Barrier
ctx.fillStyle = "#555";
if (mode === 'single') {
ctx.fillRect(barrierX - 2, 0, 4, centerY - slitWidth/2);
ctx.fillRect(barrierX - 2, centerY + slitWidth/2, 4, height);
} else {
ctx.fillRect(barrierX - 2, 0, 4, centerY - separation/2 - slitWidth/2);
ctx.fillRect(barrierX - 2, centerY - separation/2 + slitWidth/2, 4, separation - slitWidth);
ctx.fillRect(barrierX - 2, centerY + separation/2 + slitWidth/2, 4, height);
}
// Draw Sources
ctx.fillStyle = "#ffd700";
sources.forEach(s => {
ctx.beginPath();
ctx.arc(s.x, s.y, 2, 0, Math.PI * 2);
ctx.fill();
});
// Draw Huygens Wavelets
if (chkWavelets.checked) {
ctx.strokeStyle = "rgba(50, 205, 50, 0.6)";
ctx.lineWidth = 1;
const offset = (time * speed) % lambda;
const numArcs = 15;
ctx.beginPath();
sources.forEach(s => {
for(let n=1; n<=numArcs; n++) {
const r = n * lambda - offset;
if (r > 0 && r < simWidth - barrierX) {
ctx.moveTo(s.x + r * Math.cos(-Math.PI/2), s.y + r * Math.sin(-Math.PI/2));
ctx.arc(s.x, s.y, r, -Math.PI/2, Math.PI/2);
}
}
});
ctx.setLineDash([2, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
ctx.restore();
// Draw Intensity Graph
drawIntensityGraph();
}
function drawIntensityGraph() {
// Divider
ctx.beginPath();
ctx.strokeStyle = "#444";
ctx.lineWidth = 2;
ctx.moveTo(simWidth, 0);
ctx.lineTo(simWidth, height);
ctx.stroke();
ctx.fillStyle = "#888";
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Screen Plane", simWidth + GRAPH_WIDTH/2, 20);
// Graph
const startX = simWidth + 10;
const scaleI = 8;
ctx.beginPath();
ctx.strokeStyle = "#ff4d4d";
ctx.lineWidth = 2;
for (let y = 0; y < height; y++) {
const Amp = fieldBuf[y];
const I = Amp * Amp; // Intensity is Amplitude squared
const px = startX + (I * scaleI);
if (y === 0) ctx.moveTo(px, y);
else ctx.lineTo(px, y);
}
ctx.stroke();
// Baseline
ctx.beginPath();
ctx.strokeStyle = "#333";
ctx.lineWidth = 1;
ctx.moveTo(startX, 0);
ctx.lineTo(startX, height);
ctx.stroke();
}
function animate() {
time++;
lambda = parseInt(sliderLambda.value);
slitWidth = parseInt(sliderWidth.value);
separation = parseInt(sliderSep.value);
valLambda.textContent = lambda;
valWidth.textContent = slitWidth;
valSep.textContent = separation;
updateSources();
calculateField();
drawOverlay();
animationId = requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
init();
})();
</script>
</body>
</html>
```
## Doppler Effect
The Doppler effect is a well-known phenomenon where the frequency of a wave changes due to the relative motion between the source and observer. The Huygens–Fresnel principle provides a theoretical framework for understanding the Doppler effect, showing how the wavefronts shift and compress or expand based on the motion of the source and observer.
This effect can be visualized with the following animations (source: Wikipedia):
:::{layout-ncol="2"}




:::
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Doppler Effect Simulation</title>
<style>
.doppler-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.doppler-app * {
box-sizing: border-box;
}
.app-header {
text-align: center;
margin-bottom: 20px;
}
.app-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.6rem;
}
.canvas-container {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
}
#dopplerCanvas {
background-color: #ffffff;
cursor: default;
width: 100%;
height: 400px;
display: block;
}
/* Overlay info like current Mach number */
.info-overlay {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 4px;
font-size: 0.9rem;
pointer-events: none;
border: 1px solid #ddd;
font-family: monospace;
color: #007bff;
font-weight: bold;
}
.controls-panel {
background-color: #fff;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.slider-row {
display: flex;
flex-direction: column;
gap: 5px;
}
.slider-row label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
display: flex;
justify-content: space-between;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
.buttons-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
border-top: 1px solid #eee;
padding-top: 15px;
}
.preset-btn {
padding: 8px 16px;
background: #e9ecef;
border: 1px solid #ced4da;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
color: #495057;
}
.preset-btn:hover {
background: #dee2e6;
border-color: #adb5bd;
}
.control-btn {
padding: 8px 24px;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
font-weight: 600;
color: white;
min-width: 100px;
}
.btn-play { background-color: #28a745; }
.btn-play:hover { background-color: #218838; }
.btn-pause { background-color: #dc3545; }
.btn-pause:hover { background-color: #c82333; }
.btn-reset { background-color: #6c757d; }
.btn-reset:hover { background-color: #5a6268; }
</style>
</head>
<body>
<div class="doppler-app">
<div class="app-header">
<h2>Doppler Effect Simulation</h2>
</div>
<div class="canvas-container">
<canvas id="dopplerCanvas"></canvas>
<div class="info-overlay" id="status-display">Mach: 0.00</div>
</div>
<div class="controls-panel">
<div class="slider-row">
<label>
Source Velocity (Mach Number)
<span id="val-mach" style="color: #007bff;">0.00</span>
</label>
<input type="range" id="slider-mach" min="0" max="2.5" step="0.1" value="0">
</div>
<div class="buttons-row">
<button class="preset-btn" onclick="setMach(0)">Stationary (v=0)</button>
<button class="preset-btn" onclick="setMach(0.5)">Subsonic (v=0.5c)</button>
<button class="preset-btn" onclick="setMach(1.0)">Sonic Boom (v=1.0c)</button>
<button class="preset-btn" onclick="setMach(1.5)">Supersonic (v=1.5c)</button>
<button class="preset-btn" onclick="setMach(2.0)">Mach 2.0</button>
</div>
<div class="buttons-row" style="border: none; padding-top: 5px;">
<button id="btn-play" class="control-btn btn-pause">Pause</button>
<button id="btn-reset" class="control-btn btn-reset">Reset</button>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('dopplerCanvas');
const ctx = canvas.getContext('2d');
const statusDisplay = document.getElementById('status-display');
// Controls
const sliderMach = document.getElementById('slider-mach');
const valMach = document.getElementById('val-mach');
const btnPlay = document.getElementById('btn-play');
const btnReset = document.getElementById('btn-reset');
// Physics Constants
const WAVE_SPEED = 2.0; // Pixels per frame
const EMISSION_INTERVAL = 15; // Frames between wave emissions
// State
let width, height;
let isRunning = true;
let frameCount = 0;
// Simulation State
let machNumber = 0.0;
let sourceX = 0;
let sourceY = 0;
// Array of wave objects: { x: number, y: number, r: number, opacity: number }
let waves = [];
function resize() {
const rect = canvas.getBoundingClientRect();
// High DPI support
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
// Logical size
width = rect.width;
height = rect.height;
// Reset position to center if stationary, or left if moving
resetSim();
}
function resetSim() {
waves = [];
sourceY = height / 2;
// Initial position depends on speed strategy.
// For this sim, we will start center-left to allow movement to the right.
if (machNumber === 0) {
sourceX = width / 2;
} else {
sourceX = width * 0.2;
}
}
function update() {
if (!isRunning) return;
// Update Waves (Expansion)
for (let i = waves.length - 1; i >= 0; i--) {
waves[i].r += WAVE_SPEED;
waves[i].opacity -= 0.002; // Slow fade
if (waves[i].opacity <= 0 || waves[i].r > width * 1.5) {
waves.splice(i, 1);
}
}
// Emit new wave
if (frameCount % EMISSION_INTERVAL === 0) {
waves.push({
x: sourceX,
y: sourceY,
r: 0,
opacity: 1.0
});
}
// Move Source
const sourceSpeed = machNumber * WAVE_SPEED;
sourceX += sourceSpeed;
// Loop / Auto-reset logic
// If source goes too far off screen, reset to start to create a loop effect
if (sourceX > width + 100) {
// Soft reset: Move source back, clear waves that are too far gone?
// Or hard reset. Hard reset is cleaner for loop visualization.
resetSim();
}
frameCount++;
}
function draw() {
// Clear
ctx.clearRect(0, 0, width, height);
// Draw Grid (Optional, faint)
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
ctx.beginPath();
for(let x=0; x<width; x+=50) { ctx.moveTo(x,0); ctx.lineTo(x,height); }
for(let y=0; y<height; y+=50) { ctx.moveTo(0,y); ctx.lineTo(width,y); }
ctx.stroke();
// Draw Waves
ctx.lineWidth = 2;
for (const wave of waves) {
ctx.beginPath();
ctx.arc(wave.x, wave.y, wave.r, 0, Math.PI * 2);
// Color logic: blue-ish
ctx.strokeStyle = `rgba(0, 123, 255, ${wave.opacity})`;
ctx.stroke();
}
// Draw Source
ctx.beginPath();
ctx.arc(sourceX, sourceY, 6, 0, Math.PI * 2);
ctx.fillStyle = '#dc3545'; // Red
ctx.fill();
// Draw Velocity Vector (Visual aid)
if (machNumber > 0) {
ctx.beginPath();
ctx.moveTo(sourceX, sourceY);
ctx.lineTo(sourceX + 30, sourceY);
ctx.strokeStyle = '#dc3545';
ctx.lineWidth = 2;
ctx.stroke();
// Arrowhead
ctx.beginPath();
ctx.moveTo(sourceX + 30, sourceY);
ctx.lineTo(sourceX + 25, sourceY - 3);
ctx.lineTo(sourceX + 25, sourceY + 3);
ctx.fill();
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// --- Interaction ---
window.setMach = function(val) {
sliderMach.value = val;
updateParams();
// Force reset on major mode change to make it clean
resetSim();
};
function updateParams() {
machNumber = parseFloat(sliderMach.value);
valMach.textContent = machNumber.toFixed(2);
statusDisplay.textContent = `Mach: ${machNumber.toFixed(2)}`;
// Dynamic labels based on physics
if (machNumber === 0) statusDisplay.textContent += " (Stationary)";
else if (machNumber < 1) statusDisplay.textContent += " (Subsonic)";
else if (Math.abs(machNumber - 1.0) < 0.05) statusDisplay.textContent += " (Sonic Boom)";
else statusDisplay.textContent += " (Supersonic)";
}
sliderMach.addEventListener('input', updateParams);
btnPlay.addEventListener('click', () => {
isRunning = !isRunning;
if (isRunning) {
btnPlay.textContent = "Pause";
btnPlay.classList.remove('btn-play');
btnPlay.classList.add('btn-pause');
} else {
btnPlay.textContent = "Resume";
btnPlay.classList.remove('btn-pause');
btnPlay.classList.add('btn-play');
}
});
btnReset.addEventListener('click', () => {
resetSim();
if (!isRunning) {
draw();
}
});
window.addEventListener('resize', resize);
// Init
resize();
loop();
})();
</script>
</body>
</html>
```
#### Vapor cone ([Wikipedia](https://en.wikipedia.org/wiki/Vapor_cone))
The vapor cone, also known as the shock collar or shock egg, is a visible cloud of condensed water droplets that can sometimes form around an object moving at high speeds. This phenomenon is often associated with supersonic aircraft and other high-speed vehicles, where the pressure difference between the front and rear of the object leads to the condensation of water vapor in the air.

#### Playground for waves:
- [Wave applet](https://phet.colorado.edu/sims/html/wave-on-a-string/latest/wave-on-a-string_all.html)
- [Heat versus Wave Equation](https://x.com/gabrielpeyre/status/1765255995809305064?t=pzuMHd7gpN1penkJplRkQg)
- [Video of Doppler Effect on the street](https://youtu.be/ffg4TOpXZyg?si=PxLSJtInPxolERYb)
- [Doppler Effect simulator](https://ophysics.com/waves11.html)
- [Interference applet](https://phet.colorado.edu/sims/html/wave-interference/latest/wave-interference_all.html)
## Cummulative wave
A supersonic plane is flying at a speed greater than the speed of sound, emitting sound waves that reach an observer at a fixed time. The animation shows the plane's trajectory, the sound waves emitted at different times, and the impulses that reach the observer simultaneously.
```{=html}
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Symulacja Fali Skumulowanej</title>
<style>
.sim-app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
max-width: 800px;
margin: 0 auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
color: #333;
box-sizing: border-box;
}
.sim-header {
text-align: center;
margin-bottom: 20px;
}
.sim-header h2 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.5rem;
}
.sim-header p {
font-size: 0.9rem;
color: #666;
margin: 0;
}
.canvas-container {
width: 100%;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
}
#simCanvas {
background-color: #ffffff;
cursor: default;
width: 100%;
height: 500px;
display: block;
}
.controls-container {
border-top: 1px solid #eee;
padding-top: 15px;
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
}
.speed-control {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.speed-control label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
.buttons {
display: flex;
justify-content: center;
gap: 15px;
}
.btn {
padding: 8px 24px;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
font-weight: 600;
color: white;
transition: opacity 0.2s;
}
.btn:hover { opacity: 0.9; }
.btn-play { background-color: #28a745; }
.btn-pause { background-color: #dc3545; }
.btn-reset { background-color: #6c757d; }
.status-bar {
margin-top: 5px;
text-align: center;
font-family: monospace;
font-size: 1rem;
color: #007bff;
}
</style>
</head>
<body>
<div class="sim-app">
<div class="sim-header">
<h2>Simultaneous Arrival (Cumulative Wave)</h2>
<p>Supersonic plane (v=2c) approaching an observer</p>
</div>
<div class="canvas-container">
<canvas id="simCanvas"></canvas>
</div>
<div class="status-bar" id="timeDisplay">Time: 0.00s / 5.00s</div>
<div class="controls-container">
<div class="speed-control">
<label>Prędkość animacji: <span id="speedVal">1.0x</span></label>
<input type="range" id="speedSlider" min="0.1" max="2.0" step="0.1" value="1.0">
</div>
<div class="buttons">
<button id="btnPlay" class="btn btn-play">Start</button>
<button id="btnReset" class="btn btn-reset">Reset</button>
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const timeDisplay = document.getElementById('timeDisplay');
const btnPlay = document.getElementById('btnPlay');
const btnReset = document.getElementById('btnReset');
const speedSlider = document.getElementById('speedSlider');
const speedVal = document.getElementById('speedVal');
// Physics Parameters
const c = 1.0; // Speed of sound
const v = 2.0; // Speed of plane
const T = 5.0; // Impact time
const IMPULSE_INTERVAL = 0.5; // Seconds
// State
let currentTime = 0;
let isRunning = false;
let animationId;
let width, height;
let scale = 1;
let simSpeed = 1.0;
// Data structures
let impulses = [];
// Pre-calculate trajectory for static line drawing
const trajectoryPoints = [];
function precomputeTrajectory() {
const steps = 500;
for(let i=0; i<=steps; i++) {
const t = (i / steps) * T;
const safeT = Math.min(t, T - 1e-4);
const r = c * (T - safeT);
const term = Math.sqrt(v*v - c*c) / c;
const theta = term * Math.log(T / (T - safeT));
const x = r * Math.cos(theta);
const y = r * Math.sin(theta);
trajectoryPoints.push({x, y});
}
}
function resize() {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
width = rect.width;
height = rect.height;
const maxCoord = c * T * 1.1;
const minDim = Math.min(width, height);
scale = (minDim / 2) / maxCoord;
drawFrame();
}
function getPlanePos(t) {
const safeT = Math.min(t, T - 1e-6);
const r = c * (T - safeT);
const term = Math.sqrt(v*v - c*c) / c;
const theta = term * Math.log(T / (T - safeT));
return {
x: r * Math.cos(theta),
y: r * Math.sin(theta)
};
}
function update(dt) {
if (!isRunning) return;
// Apply speed multiplier
const step = dt * simSpeed;
const prevTime = currentTime;
currentTime += step;
if (currentTime >= T) {
currentTime = T;
isRunning = false;
btnPlay.textContent = "Replay";
btnPlay.classList.remove('btn-pause');
btnPlay.classList.add('btn-play');
}
// Check for new impulse emission
const startIdx = Math.floor(prevTime / IMPULSE_INTERVAL) + 1;
const endIdx = Math.floor(currentTime / IMPULSE_INTERVAL);
for(let i = startIdx; i <= endIdx; i++) {
const emitTime = i * IMPULSE_INTERVAL;
if (emitTime < T) {
const pos = getPlanePos(emitTime);
impulses.push({
emitTime: emitTime,
x: pos.x,
y: pos.y
});
}
}
}
function drawFrame() {
ctx.clearRect(0, 0, width, height);
// --- TRANSFORMED WORLD SPACE ---
ctx.save();
ctx.translate(width/2, height/2);
ctx.scale(scale, -scale);
// 1. Draw Static Trajectory (Blue dashed)
ctx.beginPath();
ctx.strokeStyle = 'rgba(0, 0, 255, 0.4)';
ctx.lineWidth = 2 / scale;
ctx.setLineDash([5 / scale, 5 / scale]);
if (trajectoryPoints.length > 0) {
ctx.moveTo(trajectoryPoints[0].x, trajectoryPoints[0].y);
for(let i=1; i<trajectoryPoints.length; i++) {
ctx.lineTo(trajectoryPoints[i].x, trajectoryPoints[i].y);
}
}
ctx.stroke();
ctx.setLineDash([]);
// 2. Draw Observer (Dot only)
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(0, 0, 0.15, 0, Math.PI*2);
ctx.fill();
// 3. Draw Impulses (Red Circles)
ctx.lineWidth = 1.5 / scale;
ctx.strokeStyle = 'rgba(255, 0, 0, 0.6)';
for (let imp of impulses) {
const r = c * (currentTime - imp.emitTime);
if (r > 0) {
ctx.beginPath();
ctx.arc(imp.x, imp.y, r, 0, Math.PI*2);
ctx.stroke();
}
}
// 4. Draw Plane (Black Dot)
const planePos = getPlanePos(currentTime);
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(planePos.x, planePos.y, 0.1, 0, Math.PI*2);
ctx.fill();
ctx.restore();
// --- END TRANSFORMED SPACE ---
// 5. Draw Labels (Screen Space - No scaling artifacts)
ctx.fillStyle = '#333';
ctx.font = '14px sans-serif';
// Observer label at center + offset
ctx.fillText("Observer", width/2 + 10, height/2 - 10);
timeDisplay.textContent = `Time: ${currentTime.toFixed(2)}s / ${T.toFixed(2)}s`;
}
function loop() {
if (isRunning) {
update(0.016);
}
drawFrame();
animationId = requestAnimationFrame(loop);
}
// Interaction
speedSlider.addEventListener('input', (e) => {
simSpeed = parseFloat(e.target.value);
speedVal.textContent = simSpeed.toFixed(1) + "x";
});
btnPlay.addEventListener('click', () => {
if (currentTime >= T) {
currentTime = 0;
impulses = [];
isRunning = true;
btnPlay.textContent = "Pause";
btnPlay.classList.remove('btn-play');
btnPlay.classList.add('btn-pause');
} else {
isRunning = !isRunning;
if(isRunning) {
btnPlay.textContent = "Pause";
btnPlay.classList.remove('btn-play');
btnPlay.classList.add('btn-pause');
} else {
btnPlay.textContent = "Resume";
btnPlay.classList.remove('btn-pause');
btnPlay.classList.add('btn-play');
}
}
});
btnReset.addEventListener('click', () => {
isRunning = false;
currentTime = 0;
impulses = [];
btnPlay.textContent = "Start";
btnPlay.classList.remove('btn-pause');
btnPlay.classList.add('btn-play');
drawFrame();
});
window.addEventListener('resize', resize);
precomputeTrajectory();
resize();
loop();
})();
</script>
</body>
</html>
```